Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface IsoBuildIllustrationProps {

const STROKE_PAINT = ISO_STROKE

const LINE_PROPS = createIsoLineProps('iso-build-line', STROKE_PAINT)
const LINE_PROPS = createIsoLineProps(STROKE_PAINT, undefined, 'iso-build-line')

/** Isometric tile module the floor grid and the columns are built on. */
const TILE_WIDTH = 38
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface IsoIngestIllustrationProps {

const STROKE_PAINT = ISO_STROKE

const LINE_PROPS = createIsoLineProps('iso-ingest-line', STROKE_PAINT)
const LINE_PROPS = createIsoLineProps(STROKE_PAINT, undefined, 'iso-ingest-line')

/**
* Inline supplied illustration for the Context area - a central store
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface IsoIntegrateIllustrationProps {

const STROKE_PAINT = ISO_STROKE

const LINE_PROPS = createIsoLineProps('iso-integrate-line', STROKE_PAINT)
const LINE_PROPS = createIsoLineProps(STROKE_PAINT, undefined, 'iso-integrate-line')

/**
* Inline supplied illustration for the Integrate area - a three-tier isometric
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface IsoMonitorIllustrationProps {

const STROKE_PAINT = ISO_STROKE

const LINE_PROPS = createIsoLineProps('iso-monitor-line', STROKE_PAINT)
const LINE_PROPS = createIsoLineProps(STROKE_PAINT, undefined, 'iso-monitor-line')

/**
* Inline supplied illustration for the Monitor area - an isometric housing whose
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/workspace/[workspaceId]/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { isResourceListEmpty } from '@/app/workspace/[workspaceId]/components/resource/is-resource-list-empty'
export { ConversationListItem } from './conversation-list-item'
export type { ErrorBoundaryProps, ErrorStateProps } from './error'
export { ErrorShell, ErrorState } from './error'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { Chip, cn } from '@sim/emcn'
import { Upload } 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'

const FILES_DOCS_URL = 'https://docs.sim.ai/files'

/**
* Hairline contours, matching the tables grid's 1px `--border-1` rules and the
* Hairline contours, matching the tables grid's 1px `--border` rules and the
* knowledge mark's thinned strokes — the graphics sit one nav item apart, so a
* heavier outline here would read as a different illustration system.
*
Expand All @@ -15,7 +16,7 @@ const FILES_DOCS_URL = 'https://docs.sim.ai/files'
* solid mid-grey body made this the heaviest thing on the page.
*/
const HAIRLINE = {
stroke: 'var(--border-1)',
stroke: 'var(--border)',
strokeWidth: 1.1,
strokeLinejoin: 'round' as const,
} as const
Expand Down Expand Up @@ -48,12 +49,13 @@ const FOLDER_FADE =
/**
* A folder held open with sheets standing proud of its front panel.
*
* Depth is carried by the surface ramp rather than by shadow: the back panel is the
* darkest tier, the sheets the lightest, the front panel between them. Shadows
* Depth is carried by the surface ramp rather than by shadow: the back panel sits a
* tier down on `--surface-4`, everything in front of it on `--surface-2`. Shadows
* would need separate light and dark recipes; the ramp inverts on its own.
*
* Every outer corner shares the same 10-unit radius so the silhouette reads as a
* single drawn shape — mixing radii makes the corners fight each other at this size.
* Unlike the tables grid — where a skeleton bar has no outline and so needs mid-grey
* ink to survive a light page — the hairline draws this shape, so the fills only have
* to separate one layer from the next and a near-white tier carries it.
*/
function FilesGraphic() {
return (
Expand All @@ -64,7 +66,7 @@ function FilesGraphic() {
fill='none'
aria-hidden='true'
focusable='false'
className={cn('block max-w-none shrink-0', FOLDER_FADE)}
className={cn('block max-w-none shrink-0', FOLDER_FADE, MASK_NO_REPEAT)}
>
<path d={FOLDER_BACK} fill='var(--surface-4)' {...HAIRLINE} />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ISO_FILL_PULSE_LOW,
ISO_STROKE as ISO_STROKE_BASE,
} from '@/components/iso/iso-illustration-style'
import { MASK_NO_REPEAT } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/mask'

const COS_30 = Math.cos(Math.PI / 6)

Expand Down Expand Up @@ -61,14 +62,14 @@ const SLABS: Box[] = [0, 90, 180].map((offset) => ({
* Lighter and thinner than the landing marks draw them.
*
* Those marks are the focal art of their section; here the mark sits beside a
* ruled grid and a skeleton feed whose lines are 1px of `--border-1`. Carrying
* ruled grid and a skeleton feed whose lines are 1px of `--border`. Carrying
* the landing's full-weight contour made the volumes read as ink next to those,
* so the shared stroke is mixed toward `--border-1` and thinned to land near a
* so the shared stroke is mixed toward `--border` and thinned to land near a
* hairline once the mark is scaled to empty-state size. Only the width diverges
* from the shared recipe; the fills are imported so a change to the iso ramp
* reaches this mark too.
*/
const ISO_STROKE = `color-mix(in srgb, ${ISO_STROKE_BASE} 55%, var(--border-1))`
const ISO_STROKE = `color-mix(in srgb, ${ISO_STROKE_BASE} 55%, var(--border))`
/** Darker than any outer face — the bore's wall turns away from the light. */
const ISO_FILL_BORE = ISO_FILL_PULSE_LOW
const KNOWLEDGE_STROKE_WIDTH = 1.9
Expand Down Expand Up @@ -145,11 +146,7 @@ const STACK_FADE =
'[-webkit-mask-image:linear-gradient(to_right,#000_56%,transparent_100%),linear-gradient(to_bottom,transparent_0%,#000_40%)] [mask-image:linear-gradient(to_right,#000_56%,transparent_100%),linear-gradient(to_bottom,transparent_0%,#000_40%)] [-webkit-mask-composite:source-in] [mask-composite:intersect]'

/** Shared iso contour recipe at this mark's own weight; spread onto both paths and circles. */
const LINE_PROPS = createIsoLineProps<SVGPathElement & SVGCircleElement>(
undefined,
ISO_STROKE,
KNOWLEDGE_STROKE_WIDTH
)
const LINE_PROPS = createIsoLineProps(ISO_STROKE, KNOWLEDGE_STROKE_WIDTH)

/**
* Down the hole: the near mouth is floored with the wall tone, then the far mouth
Expand Down Expand Up @@ -198,7 +195,7 @@ export function KnowledgeIsoMark() {
fill='none'
aria-hidden='true'
focusable='false'
className={cn('block max-w-none shrink-0', STACK_FADE)}
className={cn('block max-w-none shrink-0', STACK_FADE, MASK_NO_REPEAT)}
>
<defs>
<mask id={BORE_MASK_ID}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cn } from '@sim/emcn'
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'

const LOGS_DOCS_URL = 'https://docs.sim.ai/logs-debugging'

Expand Down Expand Up @@ -33,14 +34,14 @@ const FEED_FADE =
/** Four rows, sized to the ~148px the other resource graphics occupy so the frame centres the set alike. */
function LogsGraphic() {
return (
<div aria-hidden='true' className={cn('w-[286px]', FEED_FADE)}>
<div aria-hidden='true' className={cn('w-[286px]', FEED_FADE, MASK_NO_REPEAT)}>
{ROWS.map((row, index) => (
<div
key={row.stamp}
className={cn(
'flex items-center gap-2.5 px-2.5 py-2',
index === 0 &&
'rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-2)] shadow-card'
'rounded-[8px] border border-[var(--border)] bg-[var(--surface-2)] shadow-card'
)}
>
<span className='size-[22px] shrink-0 rounded-full bg-[var(--surface-6)]' />
Expand Down
Original file line number Diff line number Diff line change
@@ -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]'
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 (
<div aria-hidden='true' className={cn('relative h-[148px] w-[320px]', CORNER_FADE)}>
<div className='absolute top-[14px] left-[54px] rounded-tl-[6px] border-[var(--border-1)] border-t border-l'>
<div
aria-hidden='true'
className={cn('relative h-[148px] w-[320px] overflow-hidden', CORNER_FADE, MASK_NO_REPEAT)}
>
<div className='absolute top-[14px] left-[54px] rounded-tl-[6px] border-[var(--border)] border-t border-l'>
<div className='grid' style={{ gridTemplateColumns: COLUMN_TEMPLATE }}>
{HEADER_WIDTHS.map((width, column) => (
<div
key={`header-${column}`}
className='flex h-[26px] items-center border-[var(--border-1)] border-r border-b px-3'
className='flex h-[26px] items-center border-[var(--border)] border-r border-b px-3'
>
<span
className='block h-[5px] rounded-full'
Expand All @@ -78,7 +82,7 @@ function TablesGraphic() {
{row.map((width, column) => (
<div
key={`cell-${rowIndex}-${column}`}
className='relative flex h-[24px] items-center border-[var(--border-1)] border-r border-b px-3'
className='relative flex h-[24px] items-center border-[var(--border)] border-r border-b px-3'
>
<span
className='block h-[5px] rounded-full'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { isResourceListEmpty } from '@/app/workspace/[workspaceId]/components/resource/is-resource-list-empty'

/** A workspace that genuinely holds nothing — the one case that earns the graphic. */
const EMPTY = {
rowCount: 0,
isLoading: false,
isPlaceholderData: false,
error: null,
search: '',
filterCount: 0,
folderId: null,
foldersResolved: true,
} as const

describe('isResourceListEmpty', () => {
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)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/workspace/[workspaceId]/files/files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import type {
import {
EMPTY_CELL_PLACEHOLDER,
FILTER_SECTION_LABEL_CLASS,
isResourceListEmpty,
OwnerAvatar,
ownerCell,
Resource,
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -1928,6 +1928,8 @@ export function Files() {
filter={filterConfig}
/>
<Resource.Table
columns={COLUMNS}
rows={rows}
emptyState={
showEmptyState ? (
<FilesEmptyState
Expand All @@ -1936,8 +1938,6 @@ export function Files() {
/>
) : undefined
}
columns={COLUMNS}
rows={rows}
selectable={selectableConfig}
rowDragDrop={rowDragDropConfig}
onRowClick={handleRowClick}
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
import {
EMPTY_CELL_PLACEHOLDER,
FILTER_SECTION_LABEL_CLASS,
isResourceListEmpty,
OwnerAvatar,
ownerCell,
Resource,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1368,13 +1368,13 @@ export function Knowledge() {
filter={filterConfig}
/>
<Resource.Table
columns={COLUMNS}
rows={rows}
emptyState={
showEmptyState ? (
<KnowledgeEmptyState onCreate={handleOpenCreateModal} createDisabled={!canEdit} />
) : undefined
}
columns={COLUMNS}
rows={rows}
selectable={canEdit ? selectableConfig : undefined}
rowDragDrop={rowDragDropConfig}
onRowClick={handleRowClick}
Expand Down
9 changes: 6 additions & 3 deletions apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -1203,10 +1206,10 @@ export default function Logs() {
) : (
<Resource.Table
apiRef={resourceTableRef}
emptyState={showEmptyState ? <LogsEmptyState /> : undefined}
virtualized
columns={LOG_COLUMNS}
rows={rows}
emptyState={showEmptyState ? <LogsEmptyState /> : undefined}
selectedRowId={selectedLogId}
onRowClick={handleLogClick}
onRowHover={handleLogHover}
Expand Down
Loading
Loading