@ekanos/cli 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/commands/init.d.ts +36 -0
- package/dist/commands/init.js +126 -10
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/upgrade.js +108 -7
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/copy-tree.d.ts +48 -0
- package/dist/copy-tree.js +55 -0
- package/dist/copy-tree.js.map +1 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/parse-argv.d.ts +7 -1
- package/dist/parse-argv.js +10 -2
- package/dist/parse-argv.js.map +1 -1
- package/dist/project-checks.d.ts +9 -0
- package/dist/project-checks.js +29 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/project.d.ts +23 -0
- package/dist/project.js +66 -1
- package/dist/project.js.map +1 -1
- package/dist/publish-api.d.ts +1 -1
- package/dist/publish-api.js +2 -2
- package/dist/publish-api.js.map +1 -1
- package/dist/safe-fs.d.ts +1 -1
- package/dist/safe-fs.js.map +1 -1
- package/dist/sources-api.d.ts +1 -1
- package/dist/sources-api.js.map +1 -1
- package/dist/validate-findings.js +2 -1
- package/dist/validate-findings.js.map +1 -1
- package/package.json +1 -1
- package/templates/AGENTS.md.tmpl +57 -0
- package/templates/claude-skill.md.tmpl +7 -0
- package/templates/docs/api-notes.md.tmpl +21 -0
- package/templates/mcp.json.tmpl +8 -0
- package/templates/skills/build-integration/SKILL.md +178 -0
- package/templates/skills/build-integration/references/api-docs-research.md +93 -0
- package/templates/skills/build-integration/references/api-wiring.md +94 -0
- package/templates/skills/build-integration/references/widget-design.md +132 -0
- package/templates/skills/polish-widgets/SKILL.md +139 -0
- package/templates/skills/polish-widgets/references/patterns.md +269 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: polish-widgets
|
|
3
|
+
description: Review-and-fix pass over existing widget styling. Use when the user asks to review widget styling, wants widgets or the marketplace tile to look better, says the tile design is bad or the widgets look plain/generic/flat, reports overflow or responsiveness problems, or before publishing as a final styling pass.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Polish Widgets
|
|
7
|
+
|
|
8
|
+
A widget that renders the right data can still be a bad widget: every row the
|
|
9
|
+
same weight, no element answering the widget's question, a grid of identical
|
|
10
|
+
stat chips. This skill is the fix pass — render each widget in the harness,
|
|
11
|
+
find the worst offenses, fix them, show the user before and after. The
|
|
12
|
+
creation-time design rules live in
|
|
13
|
+
`../build-integration/references/widget-design.md`; this skill assumes they
|
|
14
|
+
exist and enforces them on code already written.
|
|
15
|
+
|
|
16
|
+
## The key question
|
|
17
|
+
|
|
18
|
+
Every widget exists to answer 1–2 questions. Name the question FIRST. Its
|
|
19
|
+
answer must be the most prominent element — size, weight, position — and
|
|
20
|
+
everything else demotes to supporting context. If you cannot name the
|
|
21
|
+
question, that is the finding: ask the user what the widget is for before
|
|
22
|
+
touching its classes.
|
|
23
|
+
|
|
24
|
+
| Widget type | Key question | Most prominent element |
|
|
25
|
+
| ------------------- | ---------------------------------- | --------------------------------------------------------------------------- |
|
|
26
|
+
| KPI / stat | "How are we doing?" | The hero number: `text-3xl font-semibold tracking-tight tabular-nums` |
|
|
27
|
+
| List / feed | "What needs my attention?" | Item name `text-base font-medium` + its value, right-aligned `tabular-nums` |
|
|
28
|
+
| Chart | "Which way is it going?" | The chart plus the current value; axes and legends muted |
|
|
29
|
+
| Action card | "What can I do here?" | One clear CTA; supporting copy secondary |
|
|
30
|
+
| Status / connection | "Is it working? What's connected?" | Badge or status dot + the identity it describes |
|
|
31
|
+
|
|
32
|
+
The most common failure is uniform weight: everything `text-sm`, values styled
|
|
33
|
+
like their labels. Squint at the rendered widget — if nothing reads first,
|
|
34
|
+
there is no hierarchy, and that fix comes before any padding nit.
|
|
35
|
+
|
|
36
|
+
## Review procedure
|
|
37
|
+
|
|
38
|
+
1. **Enumerate** the widgets from `components.widgets` in `src/integration.ts`
|
|
39
|
+
— id, name, component file. Review all of them unless the user scoped it.
|
|
40
|
+
2. **Render**: `npx ekanos dev`, then the Widgets surface at
|
|
41
|
+
`http://localhost:3100/<slug>/widgets`. Drive the review from the dev
|
|
42
|
+
toolbar: Render state (loading / error / inactive / disabled / active),
|
|
43
|
+
Viewport presets (check sm, not just lg), Collapsed, Unhealthy, and the
|
|
44
|
+
theme toggle — dark mode is part of the review, not an extra.
|
|
45
|
+
3. **Assess** each widget against the checklist below and write findings per
|
|
46
|
+
widget, worst-first. Missing hierarchy outranks spacing; a broken sm layout
|
|
47
|
+
outranks a badge color.
|
|
48
|
+
4. **Fix** worst-first, one widget at a time. After each: `npx tsc --noEmit`,
|
|
49
|
+
re-render in the harness.
|
|
50
|
+
5. **Show the user** before/after for each widget — screenshots or ask them to
|
|
51
|
+
look — and get their read before the next one. Never claim a widget looks
|
|
52
|
+
better without one of the two.
|
|
53
|
+
|
|
54
|
+
## Checklist
|
|
55
|
+
|
|
56
|
+
**Hierarchy**
|
|
57
|
+
|
|
58
|
+
- The key question's answer is the visually dominant element
|
|
59
|
+
- One prominent element per row; supporting info is `text-muted-foreground`
|
|
60
|
+
- Hero/stat values match the recipe: `text-3xl font-semibold tracking-tight
|
|
61
|
+
tabular-nums` (never below `text-2xl`, and never `text-sm` on a number
|
|
62
|
+
that matters)
|
|
63
|
+
- List row titles `text-base font-medium`; subtitles `text-sm text-muted-foreground`
|
|
64
|
+
- `text-xs` only on tertiary metadata (timestamps, header badge counts)
|
|
65
|
+
|
|
66
|
+
**Tokens & color**
|
|
67
|
+
|
|
68
|
+
- Semantic tokens only — no `bg-white`, `text-gray-500`, hex, raw palette shades
|
|
69
|
+
- Primary accent on ≤10% of the widget; status through `Badge` variants
|
|
70
|
+
(`success` / `warning` / `info` / `destructive`) or a semantic status dot
|
|
71
|
+
- No two colored elements competing in one row
|
|
72
|
+
- Both themes checked with the toolbar toggle
|
|
73
|
+
|
|
74
|
+
**Layout & overflow**
|
|
75
|
+
|
|
76
|
+
- Lists cap at 5–7 visible items; more goes behind inner scroll
|
|
77
|
+
(`min-h-0 flex-1 overflow-y-auto` on the list, never the card) or "View all"
|
|
78
|
+
- `truncate` + `min-w-0` on user-supplied strings; values `shrink-0` and right-aligned
|
|
79
|
+
- The sm viewport holds: cut columns, keep meaning
|
|
80
|
+
- Sizing floors hold: rows `px-4 py-3`+, list icons `h-5 w-5`+, gaps `gap-3`+,
|
|
81
|
+
body text `text-sm`+
|
|
82
|
+
|
|
83
|
+
**States & numbers**
|
|
84
|
+
|
|
85
|
+
- All four states render (the toolbar flips loading/error/inactive; empty needs
|
|
86
|
+
a data mock) and empty is a designed state, not a blank card
|
|
87
|
+
- Digits that align use `tabular-nums`; money is right-aligned with fixed width
|
|
88
|
+
|
|
89
|
+
The canonical fix for the flat-row failure, since it is the one you will apply
|
|
90
|
+
most:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Before: three spans, one size, no first read
|
|
94
|
+
<div className="flex justify-between text-sm">
|
|
95
|
+
<span>{item.name}</span>
|
|
96
|
+
<span>{item.amount}</span>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
// After: name leads, value anchors, metadata recedes
|
|
100
|
+
<div className="flex items-center gap-3 px-4 py-3">
|
|
101
|
+
<div className="min-w-0 flex-1">
|
|
102
|
+
<p className="truncate text-base font-medium">{item.name}</p>
|
|
103
|
+
<p className="text-muted-foreground text-sm">{item.detail}</p>
|
|
104
|
+
</div>
|
|
105
|
+
<span className="shrink-0 text-base font-semibold tabular-nums">
|
|
106
|
+
{formatCurrency(item.amount)}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Full before→after gallery — hero KPI, status treatment, sizing floors, inner
|
|
112
|
+
scroll, row actions, designed empty state — in `references/patterns.md`. Read
|
|
113
|
+
it before rewriting any widget body.
|
|
114
|
+
|
|
115
|
+
## The marketplace tile
|
|
116
|
+
|
|
117
|
+
"The tile looks bad" is usually not a styling bug — it is missing branding.
|
|
118
|
+
The tile is `components.marketplaceTile` in the definition, typically built on
|
|
119
|
+
`BaseMarketplaceTile` from `@ekanos/sdk/components`, and it renders from the
|
|
120
|
+
props you pass: `name`, `summary` (1–2 sentences shown on the card),
|
|
121
|
+
`brandName`, `logoPath`, `categories`, `productColor`, `ctaText`. Omit
|
|
122
|
+
`logoPath` and the card falls back to a single-letter monogram; omit `summary`
|
|
123
|
+
and the body sits blank next to its neighbours. Check the props before
|
|
124
|
+
touching CSS.
|
|
125
|
+
|
|
126
|
+
Review it at `http://localhost:3100/<slug>/tile` — the harness renders it
|
|
127
|
+
inside the real catalogue grid with two inert neighbour cards, which is the
|
|
128
|
+
only honest test: does the title wrap at column width, does the summary clamp
|
|
129
|
+
like the neighbours', is the card the same height? (Register the component as
|
|
130
|
+
`tile:` in `harness.config.ts` if the surface says none is registered.) The
|
|
131
|
+
harness also notes this surface is lightly used by the live marketplace today
|
|
132
|
+
— keep it correct and branded, don't gold-plate it.
|
|
133
|
+
|
|
134
|
+
## Reporting
|
|
135
|
+
|
|
136
|
+
Finish with a per-widget summary: the key question you assigned it, the
|
|
137
|
+
findings fixed (worst-first), anything deliberately left, and confirmation the
|
|
138
|
+
user saw the result. Then run the standard gate: `npx tsc --noEmit` and
|
|
139
|
+
`npx ekanos validate` clean.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Widget fix patterns — before → after
|
|
2
|
+
|
|
3
|
+
Each pattern names the failure, then shows the smallest honest fix. All
|
|
4
|
+
imports are `@ekanos/ui/*`; icons go through `@ekanos/ui/icon` (Font Awesome
|
|
5
|
+
names — verify against the Free set). Everything sits inside the standard
|
|
6
|
+
`Widget.Active` body from the SDK's compound API; these are the bodies, not
|
|
7
|
+
the wrapper.
|
|
8
|
+
|
|
9
|
+
## 1. Flat list row → row with a first read
|
|
10
|
+
|
|
11
|
+
**Failure:** every field the same size, so nothing answers "what needs my
|
|
12
|
+
attention?". Values styled like labels, metadata styled like values.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
// BEFORE — three spans, one weight
|
|
16
|
+
{
|
|
17
|
+
invoices.map((inv) => (
|
|
18
|
+
<div key={inv.id} className="flex justify-between p-2 text-sm">
|
|
19
|
+
<span>{inv.customer}</span>
|
|
20
|
+
<span className="text-xs">{inv.dueDate}</span>
|
|
21
|
+
<span>{inv.amount}</span>
|
|
22
|
+
</div>
|
|
23
|
+
));
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// AFTER — name leads, value anchors right, metadata recedes
|
|
29
|
+
<div className="divide-border divide-y">
|
|
30
|
+
{invoices.map((inv) => (
|
|
31
|
+
<div key={inv.id} className="flex items-center gap-3 px-4 py-3">
|
|
32
|
+
<div className="min-w-0 flex-1">
|
|
33
|
+
<p className="truncate text-base font-medium">{inv.customer}</p>
|
|
34
|
+
<p className="text-muted-foreground text-sm">
|
|
35
|
+
Due {formatDate(inv.dueDate)}
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
<span className="shrink-0 text-right text-base font-semibold tabular-nums">
|
|
39
|
+
{formatCurrency(inv.amount)}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Rules at work: `min-w-0 flex-1` + `truncate` so long names never push the
|
|
47
|
+
value out; `shrink-0` + `tabular-nums` so amounts hold a column; rows own
|
|
48
|
+
their padding (`px-4 py-3`), the container owns none.
|
|
49
|
+
|
|
50
|
+
## 2. Bare stat grid → hero KPI with supporting stats
|
|
51
|
+
|
|
52
|
+
**Failure:** four equal chips. The widget's one question ("how are we
|
|
53
|
+
doing?") has no answer — every number claims the same importance.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// BEFORE — nothing is the headline
|
|
57
|
+
<div className="grid grid-cols-2 gap-2 p-4">
|
|
58
|
+
<div className="text-sm">Revenue: {revenue}</div>
|
|
59
|
+
<div className="text-sm">Orders: {orders}</div>
|
|
60
|
+
<div className="text-sm">Avg: {avg}</div>
|
|
61
|
+
<div className="text-sm">Refunds: {refunds}</div>
|
|
62
|
+
</div>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// AFTER — one hero, a delta, the rest demoted to a footer strip
|
|
67
|
+
<div className="flex flex-1 flex-col items-center justify-center px-4 py-6">
|
|
68
|
+
<span className="text-muted-foreground text-sm">Revenue this month</span>
|
|
69
|
+
<span className="text-3xl font-semibold tracking-tight tabular-nums">
|
|
70
|
+
{formatCurrency(revenue)}
|
|
71
|
+
</span>
|
|
72
|
+
<span
|
|
73
|
+
className={cn(
|
|
74
|
+
'mt-1 text-sm font-medium',
|
|
75
|
+
delta >= 0 ? 'text-success-strong' : 'text-destructive',
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{delta >= 0 ? '+' : ''}
|
|
79
|
+
{delta.toFixed(1)}% vs last month
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div className="flex shrink-0 items-center border-t">
|
|
84
|
+
{[
|
|
85
|
+
{ label: 'Orders', value: orders },
|
|
86
|
+
{ label: 'Avg order', value: formatCurrency(avg) },
|
|
87
|
+
{ label: 'Refunds', value: refunds },
|
|
88
|
+
].map((stat, i) => (
|
|
89
|
+
<Fragment key={stat.label}>
|
|
90
|
+
{i > 0 && <div className="bg-border h-8 w-px" />}
|
|
91
|
+
<div className="flex flex-1 flex-col items-center py-3">
|
|
92
|
+
<span className="text-lg font-semibold tabular-nums">{stat.value}</span>
|
|
93
|
+
<span className="text-muted-foreground text-xs">{stat.label}</span>
|
|
94
|
+
</div>
|
|
95
|
+
</Fragment>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
(`cn` from `@ekanos/ui/utils`.) Pick the hero by the interview's SEE-outcome,
|
|
101
|
+
not by which number is largest. One `text-3xl` element per widget — two heroes
|
|
102
|
+
is zero heroes.
|
|
103
|
+
|
|
104
|
+
## 3. Unstyled status → semantic badge or dot
|
|
105
|
+
|
|
106
|
+
**Failure:** status as bare text, or hardcoded palette (`bg-green-500`) that
|
|
107
|
+
breaks in dark mode and off-brand tenants.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// BEFORE
|
|
111
|
+
<span className="text-xs">{sub.status}</span>
|
|
112
|
+
// or
|
|
113
|
+
<span className="rounded bg-green-500 px-1 text-white">{sub.status}</span>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
// AFTER — Badge variants carry the semantics; the map is the single source
|
|
118
|
+
import { Badge } from '@ekanos/ui/badge';
|
|
119
|
+
|
|
120
|
+
const STATUS_VARIANT = {
|
|
121
|
+
active: 'success',
|
|
122
|
+
trialing: 'info',
|
|
123
|
+
past_due: 'warning',
|
|
124
|
+
canceled: 'destructive',
|
|
125
|
+
} as const;
|
|
126
|
+
|
|
127
|
+
<Badge variant={STATUS_VARIANT[sub.status] ?? 'outline'}>
|
|
128
|
+
{STATUS_LABEL[sub.status] ?? sub.status}
|
|
129
|
+
</Badge>;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
When a badge is too loud (dense lists), use a dot plus muted text instead:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<span
|
|
136
|
+
className={cn(
|
|
137
|
+
'h-2.5 w-2.5 shrink-0 rounded-full',
|
|
138
|
+
sub.status === 'active' ? 'bg-success' : 'bg-muted-foreground/40',
|
|
139
|
+
)}
|
|
140
|
+
/>
|
|
141
|
+
<span className="text-muted-foreground text-sm">{STATUS_LABEL[sub.status]}</span>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
One status treatment per row. A row with a colored badge AND a colored icon
|
|
145
|
+
AND colored money is signalling nothing.
|
|
146
|
+
|
|
147
|
+
## 4. Cramped card → sizing floors applied
|
|
148
|
+
|
|
149
|
+
**Failure:** `p-2`, `space-y-1`, `text-xs` everywhere, `h-3 w-3` icons. Dense
|
|
150
|
+
is not information-rich; it is unreadable.
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
// BEFORE
|
|
154
|
+
<div className="space-y-1 p-2">
|
|
155
|
+
{events.map((e) => (
|
|
156
|
+
<div key={e.id} className="flex gap-1 text-xs">
|
|
157
|
+
<Icon name="fa-light fa-bolt" className="h-3 w-3" />
|
|
158
|
+
<span>{e.title}</span>
|
|
159
|
+
</div>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
// AFTER — floors: rows px-4 py-3, icons h-5 w-5, body text-sm+
|
|
166
|
+
<div className="divide-border divide-y">
|
|
167
|
+
{events.slice(0, 6).map((e) => (
|
|
168
|
+
<div key={e.id} className="flex items-start gap-3 px-4 py-3">
|
|
169
|
+
<span className="bg-muted text-muted-foreground shrink-0 rounded-full p-2">
|
|
170
|
+
<Icon name={eventIcon(e.type)} className="h-5 w-5" />
|
|
171
|
+
</span>
|
|
172
|
+
<div className="min-w-0 flex-1">
|
|
173
|
+
<p className="truncate text-base font-medium">{e.title}</p>
|
|
174
|
+
<p className="text-muted-foreground text-sm">{e.summary}</p>
|
|
175
|
+
</div>
|
|
176
|
+
<span className="text-muted-foreground shrink-0 text-xs">
|
|
177
|
+
{formatTimeAgo(e.at)}
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
If the content no longer fits after applying floors, show fewer items — the
|
|
185
|
+
floors are not negotiable, the item count is.
|
|
186
|
+
|
|
187
|
+
## 5. Overflowing card → inner scroll + "View all"
|
|
188
|
+
|
|
189
|
+
**Failure:** the list grows the card past its grid slot, or a fixed
|
|
190
|
+
`max-h-[180px]` caps it arbitrarily and clips the last row.
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// BEFORE — the card scrolls (or grows); a magic pixel height fights the grid
|
|
194
|
+
<div className="max-h-[180px] overflow-y-auto">{rows}</div>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
// AFTER — the LIST scrolls inside the flex column; the card holds its size
|
|
199
|
+
import { Button } from '@ekanos/ui/button';
|
|
200
|
+
|
|
201
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
202
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
203
|
+
<div className="divide-border divide-y">{rows}</div>
|
|
204
|
+
</div>
|
|
205
|
+
{items.length > 7 && (
|
|
206
|
+
<div className="shrink-0 border-t px-4 py-2">
|
|
207
|
+
<Button variant="ghost" size="sm" className="text-muted-foreground">
|
|
208
|
+
View all {items.length}
|
|
209
|
+
</Button>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>;
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`min-h-0` is the load-bearing class — without it the flex child refuses to
|
|
216
|
+
shrink and nothing scrolls. Test at the sm viewport preset, where the failure
|
|
217
|
+
actually shows.
|
|
218
|
+
|
|
219
|
+
## 6. Button pile-up → one action + menu
|
|
220
|
+
|
|
221
|
+
**Failure:** two or three buttons per row; the row becomes a toolbar.
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
// AFTER — one visible action, the rest behind a menu
|
|
225
|
+
import {
|
|
226
|
+
DropdownMenu,
|
|
227
|
+
DropdownMenuContent,
|
|
228
|
+
DropdownMenuItem,
|
|
229
|
+
DropdownMenuTrigger,
|
|
230
|
+
} from '@ekanos/ui/dropdown-menu';
|
|
231
|
+
|
|
232
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
233
|
+
<Button size="sm">Pay now</Button>
|
|
234
|
+
<DropdownMenu>
|
|
235
|
+
<DropdownMenuTrigger
|
|
236
|
+
render={<Button variant="ghost" size="icon" className="h-8 w-8" />}
|
|
237
|
+
>
|
|
238
|
+
<Icon name="fa-light fa-ellipsis-vertical" className="h-4 w-4" />
|
|
239
|
+
</DropdownMenuTrigger>
|
|
240
|
+
<DropdownMenuContent align="end">
|
|
241
|
+
<DropdownMenuItem>View details</DropdownMenuItem>
|
|
242
|
+
<DropdownMenuItem>Payment history</DropdownMenuItem>
|
|
243
|
+
</DropdownMenuContent>
|
|
244
|
+
</DropdownMenu>
|
|
245
|
+
</div>;
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The one visible action should be the urgent one; if no action is urgent, all
|
|
249
|
+
of them belong in the menu.
|
|
250
|
+
|
|
251
|
+
## 7. Blank empty state → designed empty state
|
|
252
|
+
|
|
253
|
+
**Failure:** an empty array renders an empty card, indistinguishable from a
|
|
254
|
+
bug. Empty is a state you design, distinct from error.
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
// AFTER — icon, one sentence of what appears here, what action produces it
|
|
258
|
+
<div className="flex flex-1 flex-col items-center justify-center gap-3 p-8 text-center">
|
|
259
|
+
<div className="bg-muted flex h-16 w-16 items-center justify-center rounded-full">
|
|
260
|
+
<Icon name="fa-light fa-inbox" className="text-muted-foreground h-8 w-8" />
|
|
261
|
+
</div>
|
|
262
|
+
<p className="text-base font-medium">No invoices yet</p>
|
|
263
|
+
<p className="text-muted-foreground max-w-xs text-sm">
|
|
264
|
+
Invoices appear here as soon as your first one is issued.
|
|
265
|
+
</p>
|
|
266
|
+
</div>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
No CTA unless there is a real action the user can take from the dashboard.
|