Skip to content

Commit e6a282c

Browse files
feat(dashboards): add empty-state graphic for the dashboard page
Claude-Session: https://claude.ai/code/session_01J7A6CWpREr1jiPAdWyTQbA
1 parent ea88578 commit e6a282c

3 files changed

Lines changed: 133 additions & 5 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { cn } from '@sim/emcn'
2+
import { EmptyState } from '@/components/empty-state/empty-state'
3+
import { HAIRLINE } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/hairline'
4+
import { MASK_NO_REPEAT } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/mask'
5+
6+
/**
7+
* Skeleton ink — see the `INK` note in `tables-empty-state.tsx` for why not the surface
8+
* ramp. `line` is the tables selection ring's opaque mix, so a chart stroke reads as the
9+
* one drawn mark rather than more placeholder.
10+
*/
11+
const INK = {
12+
strong: 'color-mix(in srgb, var(--text-secondary) 30%, transparent)',
13+
soft: 'color-mix(in srgb, var(--text-secondary) 15%, transparent)',
14+
area: 'color-mix(in srgb, var(--text-secondary) 7%, transparent)',
15+
line: 'color-mix(in srgb, var(--text-secondary) 46%, var(--bg))',
16+
} as const
17+
18+
/** The tables grid's corner fade: the trend panel keeps its meaning cropped. */
19+
const CORNER_FADE =
20+
'[-webkit-mask-image:linear-gradient(to_right,#000_62%,transparent_100%),linear-gradient(to_bottom,#000_56%,transparent_100%)] [mask-image:linear-gradient(to_right,#000_62%,transparent_100%),linear-gradient(to_bottom,#000_56%,transparent_100%)] [-webkit-mask-composite:source-in] [mask-composite:intersect]'
21+
22+
type Point = readonly [number, number]
23+
24+
const SPARKLINE: Point[] = [
25+
[52, 62],
26+
[66, 56],
27+
[80, 59],
28+
[94, 50],
29+
[108, 53],
30+
[122, 44],
31+
[136, 47],
32+
[150, 40],
33+
]
34+
35+
const TREND: Point[] = [
36+
[52, 140],
37+
[82, 128],
38+
[112, 133],
39+
[142, 112],
40+
[172, 118],
41+
[202, 98],
42+
[232, 104],
43+
[262, 86],
44+
[292, 92],
45+
[322, 76],
46+
]
47+
48+
const LEGEND = [
49+
{ y: 33, width: 34, fill: INK.strong },
50+
{ y: 42, width: 44, fill: INK.soft },
51+
{ y: 51, width: 28, fill: INK.soft },
52+
] as const
53+
54+
const DONUT = { cx: 202, cy: 44, r: 16, strokeWidth: 6 } as const
55+
56+
function toPath(points: Point[]): string {
57+
return points.map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x} ${y}`).join(' ')
58+
}
59+
60+
const CHART_STROKE = {
61+
stroke: INK.line,
62+
strokeLinecap: 'round',
63+
strokeLinejoin: 'round',
64+
} as const
65+
66+
/** A stat tile, a donut tile, and a wide trend panel running off two edges. */
67+
function DashboardGraphic() {
68+
const donutArc = {
69+
cx: DONUT.cx,
70+
cy: DONUT.cy,
71+
r: DONUT.r,
72+
strokeWidth: DONUT.strokeWidth,
73+
pathLength: 100,
74+
transform: `rotate(-90 ${DONUT.cx} ${DONUT.cy})`,
75+
}
76+
return (
77+
<svg
78+
viewBox='0 0 320 148'
79+
width={320}
80+
height={148}
81+
fill='none'
82+
aria-hidden='true'
83+
focusable='false'
84+
className={cn('block max-w-none shrink-0', CORNER_FADE, MASK_NO_REPEAT)}
85+
>
86+
<rect x='40' y='14' width='124' height='60' rx='6' fill='var(--surface-2)' {...HAIRLINE} />
87+
<rect x='52' y='24' width='30' height='4' rx='2' fill={INK.soft} />
88+
<rect x='52' y='33' width='48' height='8' rx='4' fill={INK.strong} />
89+
<path d={toPath(SPARKLINE)} strokeWidth={1.3} {...CHART_STROKE} />
90+
91+
<rect x='172' y='14' width='124' height='60' rx='6' fill='var(--surface-2)' {...HAIRLINE} />
92+
<circle
93+
cx={DONUT.cx}
94+
cy={DONUT.cy}
95+
r={DONUT.r}
96+
stroke={INK.soft}
97+
strokeWidth={DONUT.strokeWidth}
98+
/>
99+
<circle {...donutArc} stroke={INK.line} strokeDasharray='38 62' />
100+
<circle {...donutArc} stroke={INK.strong} strokeDasharray='24 76' strokeDashoffset={-40} />
101+
{LEGEND.map((line) => (
102+
<rect
103+
key={line.y}
104+
x='230'
105+
y={line.y}
106+
width={line.width}
107+
height='4'
108+
rx='2'
109+
fill={line.fill}
110+
/>
111+
))}
112+
113+
<rect x='40' y='82' width='300' height='90' rx='6' fill='var(--surface-2)' {...HAIRLINE} />
114+
<rect x='52' y='92' width='40' height='4' rx='2' fill={INK.strong} />
115+
<path d={`${toPath(TREND)} V 172 H 52 Z`} fill={INK.area} />
116+
<path d={toPath(TREND)} strokeWidth={1.4} {...CHART_STROKE} />
117+
</svg>
118+
)
119+
}
120+
121+
/** Empty state for a workspace whose dashboard Sim has not built yet. */
122+
export function DashboardEmptyState() {
123+
return (
124+
<EmptyState
125+
graphic={<DashboardGraphic />}
126+
title='Dashboard'
127+
description='Sim will build your dashboard here once your workspace is set up.'
128+
/>
129+
)
130+
}

‎apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { DashboardEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/dashboard-empty-state'
12
export { DocumentsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/documents-empty-state'
23
export { FilesEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/files-empty-state'
34
export { KnowledgeEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/knowledge-empty-state'

‎apps/sim/components/dashboards/dashboard-resource.tsx‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { DashboardFeatureGate } from '@/components/dashboards/dashboard-feature-gate'
44
import { DashboardLoading } from '@/components/dashboards/dashboard-loading'
55
import { DashboardPreview } from '@/components/dashboards/dashboard-preview'
6-
import { EmptyState } from '@/components/empty-state/empty-state'
6+
import { DashboardEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
77
import { Resource } from '@/app/workspace/[workspaceId]/components/resource/resource'
88
import { useWorkspaceFilesRoom } from '@/app/workspace/[workspaceId]/files/hooks/use-workspace-files-room'
99
import { useWorkspaceDashboard } from '@/hooks/queries/dashboards'
@@ -42,10 +42,7 @@ function EnabledDashboardResource({ workspaceId }: DashboardResourceProps) {
4242
/>
4343
</div>
4444
) : (
45-
<EmptyState
46-
title='Dashboard'
47-
description='Sim will build your dashboard here once your workspace is set up.'
48-
/>
45+
<DashboardEmptyState />
4946
)}
5047
</Resource>
5148
)

0 commit comments

Comments
 (0)