diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/mask.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/mask.ts
new file mode 100644
index 00000000000..902a6c27e67
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/mask.ts
@@ -0,0 +1,7 @@
+/**
+ * A mask tile is sized to the element box and `mask-repeat` initially repeats, so
+ * anything drawn past that box lands in the next tile at the opaque head of the
+ * gradient. Every fade pins `no-repeat` rather than relying on its subject
+ * happening to fit inside the frame.
+ */
+export const MASK_NO_REPEAT = '[-webkit-mask-repeat:no-repeat] [mask-repeat:no-repeat]'
diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tables-empty-state.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tables-empty-state.tsx
index 5ca521d2e99..1026d13ed3d 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tables-empty-state.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tables-empty-state.tsx
@@ -2,6 +2,7 @@ import { Chip, cn } from '@sim/emcn'
import { Plus } from '@sim/emcn/icons'
import { EmptyState } from '@/components/empty-state/empty-state'
import { EmptyStateDocsLink } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/docs-link'
+import { MASK_NO_REPEAT } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/mask'
/**
* Neutral ink at two strengths.
@@ -53,13 +54,16 @@ const SELECTED_CELL = { row: 1, column: 0 } as const
/** Skeleton grid with one cell held in an edit ring, running off two edges. */
function TablesGraphic() {
return (
-
-
+
+
{HEADER_WIDTHS.map((width, column) => (
(
{
+ it('is true only when the list genuinely holds nothing', () => {
+ expect(isResourceListEmpty(EMPTY)).toBe(true)
+ })
+
+ it('is false as soon as the list has a row', () => {
+ expect(isResourceListEmpty({ ...EMPTY, rowCount: 1 })).toBe(false)
+ })
+
+ it.each([
+ ['the first load is still in flight', { isLoading: true }],
+ ['the query is serving the previous key rows', { isPlaceholderData: true }],
+ ['the load failed', { error: new Error('boom') }],
+ ['the folder tree has not resolved', { foldersResolved: false }],
+ ])('is false while %s', (_label, override) => {
+ expect(isResourceListEmpty({ ...EMPTY, ...override })).toBe(false)
+ })
+
+ it.each([
+ ['a search is active', { search: 'invoices' }],
+ ['a filter is applied', { filterCount: 1 }],
+ ['a subfolder is open', { folderId: 'folder-1' }],
+ ])('is false when %s, because the copy would be wrong', (_label, override) => {
+ expect(isResourceListEmpty({ ...EMPTY, ...override })).toBe(false)
+ })
+
+ it('ignores a whitespace-only search, which filters no rows either', () => {
+ expect(isResourceListEmpty({ ...EMPTY, search: ' ' })).toBe(true)
+ })
+
+ it('treats a list without folder navigation as resolved at the root', () => {
+ const { folderId, foldersResolved, ...withoutFolders } = EMPTY
+ expect(isResourceListEmpty(withoutFolders)).toBe(true)
+ })
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx
index 0742f9a93cc..5e26ddce699 100644
--- a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx
@@ -231,8 +231,9 @@ interface ResourceTableProps {
* region or the headers — the chrome guarantee holds — so a consumer can show
* a zero-data graphic without the list losing its structure.
*
- * The table owns the growth box the slot is centred in, so any node centres —
- * the slot does not have to carry its own `flex-1` to sit in the middle.
+ * The table gives the slot a flex column that fills the remaining scroll area, so
+ * the node decides how to sit in it rather than depending on the scroll container's
+ * own layout.
*/
emptyState?: ReactNode
}
diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
index b655e6f2d25..7c8e0825148 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
@@ -59,6 +59,7 @@ import type {
import {
EMPTY_CELL_PLACEHOLDER,
FILTER_SECTION_LABEL_CLASS,
+ isResourceListEmpty,
OwnerAvatar,
ownerCell,
Resource,
@@ -86,7 +87,6 @@ import {
} from '@/app/workspace/[workspaceId]/components/folders'
import { ResourceActionBar } from '@/app/workspace/[workspaceId]/components/resource/components/action-bar'
import { FilesEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
-import { isResourceListEmpty } from '@/app/workspace/[workspaceId]/components/resource/is-resource-list-empty'
import { DeleteConfirmModal } from '@/app/workspace/[workspaceId]/files/components/delete-confirm-modal'
import { FileRowContextMenu } from '@/app/workspace/[workspaceId]/files/components/file-row-context-menu'
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
@@ -1928,6 +1928,8 @@ export function Files() {
filter={filterConfig}
/>
) : undefined
}
- columns={COLUMNS}
- rows={rows}
selectable={selectableConfig}
rowDragDrop={rowDragDropConfig}
onRowClick={handleRowClick}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx
index 32fbed78a31..b1828f0eb09 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx
@@ -24,6 +24,7 @@ import type {
import {
EMPTY_CELL_PLACEHOLDER,
FILTER_SECTION_LABEL_CLASS,
+ isResourceListEmpty,
OwnerAvatar,
ownerCell,
Resource,
@@ -55,7 +56,6 @@ import {
} from '@/app/workspace/[workspaceId]/components/folders'
import { ResourceActionBar } from '@/app/workspace/[workspaceId]/components/resource/components/action-bar'
import { KnowledgeEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
-import { isResourceListEmpty } from '@/app/workspace/[workspaceId]/components/resource/is-resource-list-empty'
import { BaseTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
import {
CreateBaseModal,
@@ -1368,13 +1368,13 @@ export function Knowledge() {
filter={filterConfig}
/>
) : undefined
}
- columns={COLUMNS}
- rows={rows}
selectable={canEdit ? selectableConfig : undefined}
rowDragDrop={rowDragDropConfig}
onRowClick={handleRowClick}
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
index da75a34a8da..d49b7a691fa 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
@@ -57,9 +57,12 @@ import type {
SearchConfig,
SortConfig,
} from '@/app/workspace/[workspaceId]/components'
-import { Resource, type ResourceTableHandle } from '@/app/workspace/[workspaceId]/components'
+import {
+ isResourceListEmpty,
+ Resource,
+ type ResourceTableHandle,
+} from '@/app/workspace/[workspaceId]/components'
import { LogsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
-import { isResourceListEmpty } from '@/app/workspace/[workspaceId]/components/resource/is-resource-list-empty'
import { useLogFilters } from '@/app/workspace/[workspaceId]/logs/hooks/use-log-filters'
import { useSearchState } from '@/app/workspace/[workspaceId]/logs/hooks/use-search-state'
import {
@@ -1203,10 +1206,10 @@ export default function Logs() {
) : (
: undefined}
virtualized
columns={LOG_COLUMNS}
rows={rows}
+ emptyState={showEmptyState ? : undefined}
selectedRowId={selectedLogId}
onRowClick={handleLogClick}
onRowHover={handleLogHover}
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx
index a7c309907b5..783c80b3a91 100644
--- a/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/tables/tables.tsx
@@ -23,6 +23,7 @@ import type {
import {
EMPTY_CELL_PLACEHOLDER,
FILTER_SECTION_LABEL_CLASS,
+ isResourceListEmpty,
OwnerAvatar,
ownerCell,
Resource,
@@ -54,7 +55,6 @@ import {
} from '@/app/workspace/[workspaceId]/components/folders'
import { ResourceActionBar } from '@/app/workspace/[workspaceId]/components/resource/components/action-bar'
import { TablesEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
-import { isResourceListEmpty } from '@/app/workspace/[workspaceId]/components/resource/is-resource-list-empty'
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import {
@@ -1285,6 +1285,8 @@ export function Tables() {
filter={filterConfig}
/>
) : undefined
}
- columns={COLUMNS}
- rows={rows}
selectable={canEdit ? selectableConfig : undefined}
rowDragDrop={rowDragDropConfig}
onRowClick={handleRowClick}
diff --git a/apps/sim/components/iso/iso-illustration-style.ts b/apps/sim/components/iso/iso-illustration-style.ts
index a97971a06b0..8d0dca0530e 100644
--- a/apps/sim/components/iso/iso-illustration-style.ts
+++ b/apps/sim/components/iso/iso-illustration-style.ts
@@ -1,4 +1,4 @@
-import type { SVGProps } from 'react'
+import type { SVGAttributes, SVGProps } from 'react'
export const ISO_LINE_STROKE_WIDTH = 3.2
export const ISO_STROKE = 'color-mix(in srgb, var(--text-subtle) 76%, var(--text-muted))'
@@ -18,14 +18,18 @@ export const ISO_FILL_PROPS = {
/**
* Contour props for an iso face. `strokeWidth` defaults to the shared weight; a mark
* drawn at a different scale passes its own so the recipe stays single-sourced.
+ *
+ * Typed as attributes rather than `SVGProps` because it never returns a `ref` —
+ * that is the only member that would pin it to one element, and these spread onto
+ * `` and `` alike.
*/
-export function createIsoLineProps(
- className: string | undefined,
+export function createIsoLineProps(
stroke: string,
- strokeWidth: number = ISO_LINE_STROKE_WIDTH
-): SVGProps {
+ strokeWidth: number = ISO_LINE_STROKE_WIDTH,
+ className?: string
+): SVGAttributes {
return {
- ...(className ? { className } : {}),
+ className,
fill: 'none',
pathLength: 1,
pointerEvents: 'none',