Skip to content

Commit 90a28a3

Browse files
andresdjassoclaude
authored andcommitted
feat(resources): logs and files graphics, and settle the set as one collection
Logs is an activity feed — newest run lifted onto its own card, older ones settling behind it. The relative stamps are the only literal text in any of these graphics; everything else stays skeleton, so nothing here has to be translated or kept true. Files is a folder with sheets standing proud of its front panel. Depth comes from the surface ramp rather than shadow, which would need separate light and dark recipes where the ramp inverts on its own. The tab's diagonal is filleted at both ends and every outer corner shares one radius — mixing radii, or running the diagonal into square junctions, made the corners fight at this size. Consistency pass across the set: - Titles are the resource name alone. "No tables yet" earned nothing the description does not already say. - The knowledge mark is mirrored so its bore faces left. Rebuilt on the geometry rather than flipped, since a flip would have put the shading on the wrong side. Its contours are thinned and mixed toward `--border-1`: the landing marks are the focal art of their section, but this one sits beside a ruled grid whose lines are 1px, and full-weight contours read as ink next to it. - The logs feed is sized to the same ~148px footprint as the rest. The frame centres graphic and copy together, so a taller graphic pushes its title out of line with the others' and the set stops reading as one thing. - Every empty state carries its create action and a docs link, each running the same handler as the header's primary chip. - Fades run whichever way the subject recedes: the tables grid to the bottom-right, the knowledge set up and right, the logs feed down, the folder up. Fixes a duplicate React key in the knowledge mark — the volumes stack along y now, so keying on `box.x` gave every one of them `0`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 65bad8c commit 90a28a3

8 files changed

Lines changed: 228 additions & 106 deletions

File tree

Lines changed: 105 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,122 @@
1+
import { Chip, ChipLink, Upload } from '@sim/emcn'
2+
import { BookOpen } from '@sim/emcn/icons'
13
import { EmptyState } from '@/components/empty-state/empty-state'
2-
import {
3-
Bar,
4-
Vignette,
5-
} from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/vignette'
64

7-
/** Name and size skeleton widths for the files already sitting in the folder. */
8-
const FOLDER_FILES = [
9-
{ name: 84, size: 22 },
10-
{ name: 62, size: 18 },
11-
] as const
5+
const FILES_DOCS_URL = 'https://docs.sim.ai/files'
126

137
/**
14-
* A folder held open with one file still in the air above its dashed landing
15-
* slot — Files is the one resource whose empty state is really an invitation to
16-
* drop something, so the graphic draws the gesture rather than the result.
8+
* Hairline contours, matching the tables grid's 1px `--border-1` rules and the
9+
* knowledge mark's thinned strokes — the graphics sit one nav item apart, so a
10+
* heavier outline here would read as a different illustration system.
11+
*
12+
* Fills stay near the top of the surface ramp for the same reason: the border
13+
* draws the folder and the fill only has to separate one layer from the next. A
14+
* solid mid-grey body made this the heaviest thing on the page.
1715
*/
16+
const HAIRLINE = {
17+
stroke: 'var(--border-1)',
18+
strokeWidth: 1.1,
19+
strokeLinejoin: 'round' as const,
20+
} as const
21+
22+
/**
23+
* A folder held open with sheets standing proud of its front panel.
24+
*
25+
* Depth is carried by the surface ramp rather than by shadow: the back panel is
26+
* the darkest tier, the sheets are the lightest, and the front panel sits between
27+
* them. Shadows would need separate light and dark recipes; the ramp inverts on
28+
* its own.
29+
*
30+
* The tab's diagonal is filleted at both ends and every outer corner shares one
31+
* radius, so the silhouette reads as a single drawn shape. Mixing radii, or
32+
* running the diagonal into square junctions, makes the corners fight each other
33+
* at this size.
34+
*/
35+
const CORNER = 10
36+
const FOLDER_BACK = [
37+
'M 22 34',
38+
'H 66',
39+
'Q 72 34 75.5 38',
40+
'L 82.5 46',
41+
'Q 86 50 92 50',
42+
'H 178',
43+
'Q 188 50 188 60',
44+
'V 140',
45+
'Q 188 150 178 150',
46+
'H 22',
47+
'Q 12 150 12 140',
48+
'V 44',
49+
'Q 12 34 22 34',
50+
'Z',
51+
].join(' ')
52+
53+
/**
54+
* Dissolves upward, so the folder rises out of the page while its front panel
55+
* stays crisp where the copy begins. Running it the other way — the direction the
56+
* logs feed fades — ate the panel's base and left the tab hanging.
57+
*/
58+
const FOLDER_FADE =
59+
'[-webkit-mask-image:linear-gradient(to_top,#000_58%,transparent_100%)] [mask-image:linear-gradient(to_top,#000_58%,transparent_100%)]'
60+
1861
function FilesGraphic() {
1962
return (
20-
<Vignette>
21-
<span className='absolute top-[30px] left-[54px] h-[16px] w-[64px] rounded-t-[8px] border-[var(--border-1)] border-x border-t bg-[var(--surface-3)]' />
22-
23-
<div className='absolute top-[42px] left-[54px] h-[92px] w-[208px] rounded-[8px] rounded-tl-none border border-[var(--border-1)] bg-[var(--surface-2)] px-3 pt-3'>
24-
{FOLDER_FILES.map((file) => (
25-
<div key={file.name} className='mb-[10px] flex items-center gap-2'>
26-
<span className='size-[14px] shrink-0 rounded-[3px] bg-[var(--surface-5)]' />
27-
<Bar className='h-2' style={{ width: file.name }} />
28-
<Bar className='ml-auto h-2' style={{ width: file.size }} />
29-
</div>
30-
))}
31-
32-
<span className='block h-[26px] w-full rounded-[6px] border border-[var(--brand-secondary)] border-dashed opacity-60' />
33-
</div>
34-
35-
<div className='-rotate-6 absolute top-[10px] left-[178px] flex h-[36px] w-[108px] items-center gap-2 rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-1)] px-2.5 shadow-lg'>
36-
<span className='size-[16px] shrink-0 rounded-[4px] bg-[color-mix(in_srgb,var(--brand-secondary)_28%,transparent)]' />
37-
<span className='flex flex-col gap-[5px]'>
38-
<Bar className='h-2 w-[54px]' />
39-
<Bar className='h-[6px] w-[32px]' />
40-
</span>
41-
</div>
42-
</Vignette>
63+
<svg
64+
viewBox='0 0 200 160'
65+
width={185}
66+
height={148}
67+
fill='none'
68+
aria-hidden='true'
69+
focusable='false'
70+
className={`block max-w-none shrink-0 ${FOLDER_FADE}`}
71+
>
72+
<path d={FOLDER_BACK} fill='var(--surface-4)' {...HAIRLINE} />
73+
74+
<rect x='34' y='56' width='132' height='84' rx='5' fill='var(--surface-2)' {...HAIRLINE} />
75+
<rect x='26' y='64' width='148' height='76' rx='5' fill='var(--surface-2)' {...HAIRLINE} />
76+
77+
<rect
78+
x='12'
79+
y='74'
80+
width='176'
81+
height='76'
82+
rx={CORNER}
83+
fill='var(--surface-2)'
84+
{...HAIRLINE}
85+
/>
86+
</svg>
4387
)
4488
}
4589

90+
interface FilesEmptyStateProps {
91+
/** Opens the file picker — the same action the header's upload chip runs. */
92+
onUpload: () => void
93+
/** Mirrors the header chip's disabled state: no edit rights, or an upload in flight. */
94+
uploadDisabled?: boolean
95+
}
96+
4697
/** Empty state for the files list when the workspace has none. */
47-
export function FilesEmptyState() {
98+
export function FilesEmptyState({ onUpload, uploadDisabled = false }: FilesEmptyStateProps) {
4899
return (
49100
<EmptyState
50101
graphic={<FilesGraphic />}
51-
title='No files yet'
52-
description='Drop files here to share them across your team and every agent.'
102+
title='Files'
103+
description='Upload files to share them across your team and every agent.'
104+
action={
105+
<div className='flex items-center gap-2'>
106+
<Chip variant='primary' onClick={onUpload} disabled={uploadDisabled} leftIcon={Upload}>
107+
Upload
108+
</Chip>
109+
<ChipLink
110+
href={FILES_DOCS_URL}
111+
target='_blank'
112+
rel='noopener noreferrer'
113+
variant='border'
114+
leftIcon={BookOpen}
115+
>
116+
Docs
117+
</ChipLink>
118+
</div>
119+
}
53120
/>
54121
)
55122
}

apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/gallery-preview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const ENTRIES = [
1212
{ label: 'Knowledge', node: <KnowledgeEmptyState onCreate={() => {}} /> },
1313
{ label: 'Tables', node: <TablesEmptyState onCreate={() => {}} /> },
1414
{ label: 'Logs', node: <LogsEmptyState /> },
15-
{ label: 'Files', node: <FilesEmptyState /> },
15+
{ label: 'Files', node: <FilesEmptyState onUpload={() => {}} /> },
1616
{ label: 'Skills', node: <SkillsEmptyState /> },
1717
]
1818

apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/knowledge-empty-state.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function KnowledgeEmptyState({
2020
return (
2121
<EmptyState
2222
graphic={<KnowledgeIsoMark height={148} />}
23-
title='No knowledge bases yet'
23+
title='Knowledge bases'
2424
description='Upload documents to give your agents a memory they can search.'
2525
action={
2626
<div className='flex items-center gap-2'>

apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/knowledge-iso.tsx

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,37 @@ function boxFaces(box: Box) {
4242
}
4343
}
4444

45+
/**
46+
* Thin along y rather than x, so the cover is the face at max y — the one the
47+
* projection turns to the left. Stacking along y then walks the set left-and-down
48+
* toward the viewer, so drawing in offset order paints back to front.
49+
*/
4550
const SLABS: Box[] = [0, 90, 180].map((offset) => ({
46-
x: offset,
47-
y: 0,
51+
x: 0,
52+
y: offset,
4853
z: 0,
49-
w: 46,
50-
d: 208,
54+
w: 208,
55+
d: 46,
5156
h: 236,
5257
}))
5358

54-
const ISO_STROKE = 'color-mix(in srgb, var(--text-subtle) 76%, var(--text-muted))'
59+
/**
60+
* Lighter and thinner than the landing marks draw them.
61+
*
62+
* Those marks are the focal art of their section; here the mark sits beside a
63+
* ruled grid and a skeleton feed whose lines are 1px of `--border-1`. Carrying
64+
* the landing's full-weight contour made the volumes read as ink next to those,
65+
* so the stroke is mixed toward `--border-1` and thinned to land near a hairline
66+
* once the mark is scaled to empty-state size.
67+
*/
68+
const ISO_STROKE =
69+
'color-mix(in srgb, color-mix(in srgb, var(--text-subtle) 76%, var(--text-muted)) 55%, var(--border-1))'
5570
const ISO_FILL_LOW = 'var(--surface-6)'
5671
const ISO_FILL_MID = 'color-mix(in srgb, var(--surface-3) 58%, var(--surface-6))'
5772
const ISO_FILL_HIGH = 'var(--surface-3)'
5873
/** Darker than any outer face — the bore's wall turns away from the light. */
5974
const ISO_FILL_BORE = 'color-mix(in srgb, var(--surface-6) 72%, var(--surface-7))'
60-
const ISO_LINE_STROKE_WIDTH = 3.2
75+
const ISO_LINE_STROKE_WIDTH = 1.9
6176

6277
/**
6378
* Maps flat artwork into the plane of the front volume's cover.
@@ -72,25 +87,25 @@ const ISO_LINE_STROKE_WIDTH = 3.2
7287
const COVER = SLABS[SLABS.length - 1]
7388

7489
const COVER_PLANE = (() => {
75-
const [originX, originY] = project(COVER.x + COVER.w, COVER.y, COVER.z)
76-
return `matrix(${(-COS_30).toFixed(4)} 0.5 0 1 ${originX.toFixed(3)} ${(originY - COVER.h).toFixed(3)})`
90+
const [originX, originY] = project(COVER.x, COVER.y + COVER.d, COVER.z)
91+
return `matrix(${COS_30.toFixed(4)} 0.5 0 1 ${originX.toFixed(3)} ${(originY - COVER.h).toFixed(3)})`
7792
})()
7893

7994
const BORE_RADIUS = 62
80-
const BORE_CX = COVER.d / 2
95+
const BORE_CX = COVER.w / 2
8196
const BORE_CY = COVER.h / 2
8297

8398
/**
8499
* The far mouth of the bore, in the same cover-plane coordinates.
85100
*
86-
* Boring straight back through the volume is a world-space step of `-w` along x.
101+
* Boring straight back through the volume is a world-space step of `-d` along y.
87102
* Solving the cover-plane matrix for the local offset that produces that step
88-
* gives `(+w, -w)` — so the far mouth sits up and left of the near one by exactly
103+
* gives `(+d, -d)` — so the far mouth sits up and left of the near one by exactly
89104
* the volume's thickness, and the sliver of near-mouth it fails to cover is the
90105
* wall you see down the hole.
91106
*/
92-
const FAR_CX = BORE_CX + COVER.w
93-
const FAR_CY = BORE_CY - COVER.w
107+
const FAR_CX = BORE_CX + COVER.d
108+
const FAR_CY = BORE_CY - COVER.d
94109

95110
const BORE_MASK_ID = 'knowledge-iso-bore-mask'
96111
const BORE_CLIP_ID = 'knowledge-iso-bore-clip'
@@ -108,13 +123,13 @@ const VIEW_BOX = `${MIN_X.toFixed(2)} ${MIN_Y.toFixed(2)} ${(MAX_X - MIN_X).toFi
108123
* The tables grid's corner fade, run along the other diagonal.
109124
*
110125
* There it dissolves toward the bottom-right, because a grid keeps its meaning
111-
* cropped. Here the stack recedes up and to the left, and the front volume
112-
* carries the bore — so the fade is anchored at the bottom-right and eats into
113-
* the back of the set instead, reading as more volumes behind rather than
114-
* dissolving the one detail worth looking at.
126+
* cropped. Here the set recedes up and to the right, and the front volume carries
127+
* the bore — so the fade is anchored at the bottom-left and eats into the back of
128+
* the set instead, reading as more volumes behind rather than dissolving the one
129+
* detail worth looking at.
115130
*/
116131
const STACK_FADE =
117-
'[-webkit-mask-image:linear-gradient(to_right,transparent_0%,#000_44%),linear-gradient(to_bottom,transparent_0%,#000_40%)] [mask-image:linear-gradient(to_right,transparent_0%,#000_44%),linear-gradient(to_bottom,transparent_0%,#000_40%)] [-webkit-mask-composite:source-in] [mask-composite:intersect]'
132+
'[-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]'
118133

119134
const LINE_PROPS = {
120135
fill: 'none' as const,
@@ -154,9 +169,9 @@ export function KnowledgeIsoMark({ height = 148 }: KnowledgeIsoMarkProps) {
154169
{SLABS.map((box) => {
155170
const faces = boxFaces(box)
156171
return (
157-
<g key={box.x}>
158-
<path d={toPath(faces.left)} fill={ISO_FILL_LOW} stroke='none' />
159-
<path d={toPath(faces.right)} fill={ISO_FILL_MID} stroke='none' />
172+
<g key={`${box.x}-${box.y}`}>
173+
<path d={toPath(faces.left)} fill={ISO_FILL_MID} stroke='none' />
174+
<path d={toPath(faces.right)} fill={ISO_FILL_LOW} stroke='none' />
160175
<path d={toPath(faces.top)} fill={ISO_FILL_HIGH} stroke='none' />
161176
<path d={toPath(faces.left)} {...LINE_PROPS} />
162177
<path d={toPath(faces.right)} {...LINE_PROPS} />

0 commit comments

Comments
 (0)