feat: home table polish: run links, language in the path line, fork button sizing, row separators

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-10-08 21:33:44 +02:00
co-authored by Claude Opus 5.5
parent 42d5661573
commit 1c67ba47b2
8 changed files with 102 additions and 43 deletions
@@ -31,11 +31,15 @@
<!-- title on the wrapper, not on <Button>: Button renders its `title` prop only in the
<button> branch, so the <a> branch taken here (href is set) would silently drop it.
On the wrapper it also covers the icon and padding, not just the label text. -->
<div title={label}>
<!-- Like the row's Edit button: shown on hover, focus or keyboard selection. -->
<div
title={label}
class="invisible group-hover/row:visible group-focus-within/row:visible group-data-[row-keyboard-selected=true]/row:visible"
>
<Button
variant="subtle"
wrapperClasses="max-w-56"
unifiedSize="md"
variant="accent"
wrapperClasses="max-w-40"
unifiedSize="sm"
startIcon={{ icon: Pen }}
{href}
onClick={(e) => onEditInForkClick(e, itemType, path, { hasHref: true })}
@@ -17,6 +17,7 @@
EMPTY_CELL_DASH,
HOME_TABLE_BADGE_GRID,
HOME_TABLE_GRID,
HOME_TABLE_ROW_SEPARATOR,
isHomeTable
} from '$lib/components/home/homeTable'
@@ -78,7 +79,8 @@
badges?: import('svelte').Snippet
/** Badge groups the home table gives a column each; elsewhere they render
* inline, in this order, after `badges`. */
languageBadge?: import('svelte').Snippet
/** Appended to the path line, e.g. a script's language. */
pathSuffix?: string | undefined
tagBadges?: import('svelte').Snippet
draftBadge?: import('svelte').Snippet
sharedBadge?: import('svelte').Snippet
@@ -122,7 +124,7 @@
workspaceId,
depth = 0,
badges,
languageBadge,
pathSuffix = undefined,
tagBadges,
draftBadge,
sharedBadge,
@@ -206,7 +208,8 @@
data-row-keyboard-selected={keyboardSelected ? 'true' : undefined}
class={twMerge(
HOME_TABLE_GRID,
'group/row relative w-full pl-5 pr-3 py-2.5 rounded-md',
'group/row relative w-full pl-5 pr-3 py-2.5',
HOME_TABLE_ROW_SEPARATOR,
disabled ? 'opacity-25' : 'hover:bg-surface-hover',
inSelectionMode ? 'cursor-pointer select-none' : '',
rowSelection?.selected
@@ -224,8 +227,12 @@
class="absolute inset-0"
></a>
{/if}
<!-- The kind icon, swapped for the checkbox on hover and in selection mode. -->
<div class="relative z-[1] w-4 h-4">
<!-- The kind icon, swapped for the checkbox on hover and in selection mode. In the
tree it moves right by the same depth offset as the name, under its folder. -->
<div
class="relative z-[1] w-4 h-4"
style={depth > 0 ? `transform: translateX(${depth * 16}px);` : ''}
>
<div
class={twMerge(
'absolute inset-0',
@@ -425,8 +432,14 @@
'text-emphasis flex-wrap text-left text-xs min-w-0',
homeTable ? 'font-normal' : 'font-semibold',
titleBadge ? 'inline-flex items-center gap-2' : '',
homeTable && !titleBadge ? 'line-clamp-2 break-words' : '',
linked ? 'group-hover/row:underline decoration-gray-400' : ''
)}
title={homeTable && !customSummary
? !summary || summary.length == 0
? displayPath
: summary
: undefined}
>
{#if customSummary}
{@render customSummary?.()}
@@ -437,11 +450,6 @@
{/if}
{@render titleBadge?.()}
</div>
{#if !inlineBadges && languageBadge}
<div class="flex items-center shrink-0">
{@render languageBadge()}
</div>
{/if}
{#if !inlineBadges && tagBadges}
<div class="relative z-[1] flex items-center gap-1 shrink-0">{@render tagBadges()}</div>
{/if}
@@ -452,10 +460,9 @@
{#if homeTable}
{@render inlineFavorite()}
{/if}
{#if inlineBadges && (badges || languageBadge || tagBadges || draftBadge || sharedBadge || labelBadges)}
{#if inlineBadges && (badges || tagBadges || draftBadge || sharedBadge || labelBadges)}
<div class="relative z-[1] hidden lg:flex flex-row gap-1 items-center shrink-0">
{@render badges?.()}
{@render languageBadge?.()}
{@render tagBadges?.()}
{@render draftBadge?.()}
{@render sharedBadge?.()}
@@ -469,6 +476,9 @@
{:else}
{path}
{/if}
{#if pathSuffix}
<span class="opacity-75">· {pathSuffix}</span>
{/if}
</div>
</div>
{/snippet}
@@ -29,7 +29,6 @@
import { isDeployable } from '$lib/utils_deployable'
import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
import { LanguageIcon } from '../languageIcons'
import {
Archive,
Calendar,
@@ -52,7 +51,7 @@
import { Drawer, DrawerContent } from '..'
import NoMainFuncBadge from '$lib/components/NoMainFuncBadge.svelte'
import RowLabels from './RowLabels.svelte'
import { isHomeTable } from '$lib/components/home/homeTable'
import { defaultScriptLanguages } from '$lib/scripts'
import Popover from '$lib/components/Popover.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
@@ -104,7 +103,6 @@
}: Props = $props()
const dispatch = createEventDispatcher()
const iconSize = isHomeTable() ? 12 : 14
async function archiveScript(path: string): Promise<void> {
await ScriptService.archiveScriptByPath({ workspace: $workspaceStore!, path })
@@ -172,14 +170,12 @@
workspaceId={$workspaceStore ?? ''}
canFavorite={!script.draft_only}
editedAt={script.created_at}
pathSuffix={script.language
? (defaultScriptLanguages[script.language] ?? script.language)
: undefined}
{depth}
{rowSelection}
>
{#snippet languageBadge()}
{#if script.language}
<LanguageIcon lang={script.language} width={iconSize} height={iconSize} />
{/if}
{/snippet}
{#snippet tagBadges()}
{#if script.lock_error_logs}
<Badge color="red" baseClass="border border-red-200">Deployment failed</Badge>
@@ -18,7 +18,9 @@
} from '$lib/gen'
import { resource } from 'runed'
import { getDraftItems } from '$lib/workspaceDrafts.svelte'
import { disableHubStore, userStore, workspaceStore } from '$lib/stores'
import { disableHubStore, userStore, userWorkspaces, workspaceStore } from '$lib/stores'
import { editInForkAllowed } from '$lib/utils/editInFork'
import { isCloudHosted } from '$lib/cloud'
import { useOperatorBuilderFlows } from '$lib/operatorWriteRights'
import type uFuzzy from '@leeoniya/ufuzzy'
import {
@@ -67,7 +69,13 @@
import BulkActionsBar from './BulkActionsBar.svelte'
import { HomeSelection, setHomeSelection, toBulkItem } from './homeSelection.svelte'
import { HomeActivity, setHomeActivity } from './homeActivity.svelte'
import { HOME_TABLE_BADGE_GRID, HOME_TABLE_GRID, setHomeTable } from './homeTable'
import {
HOME_TABLE_BADGE_GRID,
HOME_TABLE_GRID,
HOME_TABLE_ROW_SEPARATOR,
HOME_TABLE_WIDE_ACTIONS,
setHomeTable
} from './homeTable'
import Checkbox from '../common/checkbox/Checkbox.svelte'
const operatorBuilderFlows = useOperatorBuilderFlows()
@@ -1495,6 +1503,16 @@
)
: items
)
// A row the user cannot edit offers "Edit in fork", which needs a wider actions column.
let tableStyle = $derived(
!isCloudHosted() &&
editInForkAllowed($workspaceStore, $userWorkspaces) &&
((treeView ? treeSource : items) ?? []).some(
(it) => !showEditButtons || !('canWrite' in it) || !it.canWrite
)
? HOME_TABLE_WIDE_ACTIONS
: undefined
)
// Remount identity: only a *mode* change (workspace, selected owner, entering/leaving
// search, label filter) restructures the tree, so only those key the {#key} remount.
// A sort/archive/library/kind change keeps the same lazy tree and is applied in place
@@ -2111,7 +2129,7 @@
</div>
{/if}
{:else if treeView}
<div class="border rounded-md bg-surface-tertiary">
<div class="border rounded-md bg-surface-tertiary" style={tableStyle}>
{@render tableHeader()}
<!-- Remount the tree on a MODE change only (treeKey = view/owner/search/label):
expanded folders (their `opened` state is local to each TreeView) collapse
@@ -2147,13 +2165,17 @@
{/key}
</div>
{:else}
<div class="border rounded-md bg-surface-tertiary" class:wm-imported={justImported}>
<div
class="border rounded-md bg-surface-tertiary"
class:wm-imported={justImported}
style={tableStyle}
>
{@render tableHeader()}
{#if filter === ''}
{#each [...visiblePipelineFolders].sort() as folder (folder)}
<a
href="{base}/pipeline/{encodeURIComponent(folder)}"
class="w-full inline-flex items-center gap-4 pl-5 pr-3 py-2 rounded-md hover:bg-surface-hover transition-colors text-sm"
class="w-full inline-flex items-center gap-4 pl-5 pr-3 py-2 {HOME_TABLE_ROW_SEPARATOR} hover:bg-surface-hover transition-colors text-sm"
>
<NetworkIcon size={16} class="text-emerald-600 dark:text-emerald-400" />
<span class="text-xs font-medium text-emphasis truncate">Pipeline · f/{folder}</span>
@@ -6,6 +6,9 @@
import { displayDate, formatCron } from '$lib/utils'
import { describeSchedule } from '$lib/utils/describeCron'
import { twMerge } from 'tailwind-merge'
import { ExternalLink } from 'lucide-svelte'
import { Button } from '$lib/components/common'
import { workspaceStore } from '$lib/stores'
import { getHomeActivity, type ActivityKind } from './homeActivity.svelte'
interface Props {
@@ -88,10 +91,11 @@
{#if homeActivity}
<div class="relative z-[1] hidden lg:flex items-center w-24 min-w-0 shrink-0 text-xs">
{#if lastRun}
<Tooltip class="min-w-0">
<a
href="{base}/runs/{path}"
class="flex items-center gap-1.5 min-w-0 text-secondary hover:text-primary"
<!-- The close delay lets the pointer cross into the popup to click a run. -->
<Tooltip class="min-w-0" closeDelay={150}>
<!-- Not a link: the popover's "View runs" and run entries are the ways in. -->
<span
class="flex items-center gap-1.5 min-w-0 text-secondary cursor-default"
aria-label="Latest runs"
>
<span class={twMerge('w-1.5 h-1.5 rounded-full shrink-0', statusClass[lastRun.status])}
@@ -103,16 +107,29 @@
<TimeAgo date={lastRun.created_at} compact /> ago
{/if}
</span>
</a>
</span>
{#snippet text()}
<div class="flex flex-col gap-1">
<span class="font-semibold">Latest {runs.length} runs</span>
<div class="flex flex-col gap-1 min-w-56">
<div class="flex items-center justify-between gap-4">
<span class="font-semibold">Latest {runs.length} runs</span>
<Button
variant="subtle"
unifiedSize="xs"
href="{base}/runs/{path}?workspace={$workspaceStore}"
>
View runs
</Button>
</div>
{#each runs as run (run.id)}
<div class="flex items-center gap-2">
<a
href="{base}/run/{run.id}?workspace={$workspaceStore}"
class="group/run -mx-1.5 px-1.5 py-0.5 rounded flex items-center gap-2 text-primary hover:bg-surface-hover"
>
<div class={twMerge('w-2 h-2 rounded-full', statusClass[run.status])}></div>
<span class="w-16">{statusLabel[run.status]}</span>
<span class="text-hint">{displayDate(run.created_at, true)}</span>
</div>
<span class="text-hint grow">{displayDate(run.created_at, true)}</span>
<ExternalLink size={12} class="text-hint group-hover/run:text-primary shrink-0" />
</a>
{/each}
</div>
{/snippet}
@@ -7,7 +7,7 @@
import Item from './Item.svelte'
import { countLeaves, type FolderItem, type ItemType, type UserItem } from './treeViewUtils'
import { twMerge } from 'tailwind-merge'
import { isHomeTable } from './homeTable'
import { HOME_TABLE_ROW_SEPARATOR, isHomeTable } from './homeTable'
import { pluralize } from '$lib/utils'
import { base } from '$lib/base'
import { Button } from '$lib/components/common'
@@ -74,7 +74,9 @@
}: Props = $props()
// The home table draws no separators between its rows.
const separator = isHomeTable() ? 'pl-5 rounded-md hover:bg-surface-hover' : 'border-b'
const separator = isHomeTable()
? `pl-5 hover:bg-surface-hover ${HOME_TABLE_ROW_SEPARATOR}`
: 'border-b'
let visualDepth = $derived(depth + indent)
@@ -1,4 +1,5 @@
<script lang="ts">
import { HOME_TABLE_ROW_SEPARATOR } from './homeTable'
import { untrack, type Snippet } from 'svelte'
import TreeView from './TreeView.svelte'
import { countLeaves, groupItems, type ItemType, type UserItem } from './treeViewUtils'
@@ -287,7 +288,7 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
onclick={toggleOtherUsers}
class="pl-5 pr-3 py-2 rounded-md hover:bg-surface-hover w-full flex flex-row items-center justify-between cursor-pointer"
class="pl-5 pr-3 py-2 hover:bg-surface-hover {HOME_TABLE_ROW_SEPARATOR} w-full flex flex-row items-center justify-between cursor-pointer"
>
<div class="flex flex-row items-center gap-4">
<Users size={16} class="text-secondary" />
@@ -7,12 +7,19 @@ import { getContext, setContext } from 'svelte'
* triggers and runs cells are hidden, and the template drops them too.
*/
export const HOME_TABLE_GRID =
'grid items-center gap-x-5 grid-cols-[1rem_minmax(0,1fr)_6.5rem] lg:grid-cols-[1rem_minmax(0,2fr)_minmax(0,1.5fr)_6rem_13rem_5rem_6.5rem]'
'grid items-center gap-x-5 grid-cols-[1rem_minmax(0,1fr)_var(--home-actions-w,6.5rem)] lg:grid-cols-[1rem_minmax(0,3fr)_minmax(0,1fr)_6rem_13rem_5rem_var(--home-actions-w,6.5rem)]'
/** Actions column width when some row shows "Edit in fork" instead of Edit. */
export const HOME_TABLE_WIDE_ACTIONS = '--home-actions-w: 12.5rem'
/** Sub-columns of the badges area: labels · shared. */
export const HOME_TABLE_BADGE_GRID =
'grid-cols-[minmax(0,1fr)_1.25rem] gap-x-2 items-center min-w-0'
/** The faint line between home table rows; the last row in the box draws none. */
export const HOME_TABLE_ROW_SEPARATOR =
'border-b border-gray-100 dark:border-gray-700/40 last:border-b-0'
/** Shows a muted dash in a table cell that renders no element. */
export const EMPTY_CELL_DASH =
"[&:not(:has(*))]:after:content-['–'] [&:not(:has(*))]:after:text-hint [&:not(:has(*))]:after:text-2xs [&:not(:has(*))]:after:opacity-40"