@ekanos/cli 0.3.6 → 0.4.0
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 +1 -0
- package/dist/commands/dev.js +5 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.js +8 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/upgrade.js +58 -7
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/compatibility.d.ts +4 -1
- package/dist/compatibility.js +4 -1
- package/dist/compatibility.js.map +1 -1
- package/dist/design-findings.d.ts +22 -0
- package/dist/design-findings.js +47 -0
- package/dist/design-findings.js.map +1 -0
- package/dist/harness-scaffold.d.ts +4 -0
- package/dist/harness-scaffold.js +18 -1
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/pnpm-builds.d.ts +41 -6
- package/dist/pnpm-builds.js +169 -0
- package/dist/pnpm-builds.js.map +1 -1
- package/dist/validate-findings.js +2 -0
- package/dist/validate-findings.js.map +1 -1
- package/package.json +2 -2
- package/templates/AGENTS.md.tmpl +44 -9
- package/templates/eslint.config.mjs.tmpl +62 -0
- package/templates/harness/app/[slug]/page.tsx.tmpl +1 -0
- package/templates/harness.config.entry.ts.tmpl +8 -0
- package/templates/package.json.tmpl +7 -4
- package/templates/skills/build-integration/references/widget-design.md +128 -32
- package/templates/skills/polish-widgets/SKILL.md +29 -13
- package/templates/skills/polish-widgets/references/patterns.md +90 -58
- package/templates/src/integration.ts.tmpl +50 -6
|
@@ -30,9 +30,12 @@ attention?". Values styled like labels, metadata styled like values.
|
|
|
30
30
|
|
|
31
31
|
```tsx
|
|
32
32
|
// AFTER — name leads, value anchors right, metadata recedes
|
|
33
|
-
<div className="divide-border divide-y">
|
|
33
|
+
<div className="divide-border/50 divide-y">
|
|
34
34
|
{invoices.map((inv) => (
|
|
35
|
-
<div
|
|
35
|
+
<div
|
|
36
|
+
key={inv.id}
|
|
37
|
+
className="hover:bg-accent/50 flex items-center gap-3 px-5 py-3"
|
|
38
|
+
>
|
|
36
39
|
<div className="min-w-0 flex-1">
|
|
37
40
|
<p className="truncate text-base font-medium">{inv.customer}</p>
|
|
38
41
|
<p className="text-muted-foreground text-sm">
|
|
@@ -49,7 +52,10 @@ attention?". Values styled like labels, metadata styled like values.
|
|
|
49
52
|
|
|
50
53
|
Rules at work: `min-w-0 flex-1` + `truncate` so long names never push the
|
|
51
54
|
value out; `shrink-0` + `tabular-nums` so amounts hold a column; rows own
|
|
52
|
-
their padding (`px-
|
|
55
|
+
their padding (`px-5 py-3`), the container owns none — so the hover band and
|
|
56
|
+
the separators bleed to the card's edge. The hover is a flat
|
|
57
|
+
`hover:bg-accent/50` band; a `rounded-lg` pill with side padding is the drift
|
|
58
|
+
this pattern exists to prevent.
|
|
53
59
|
|
|
54
60
|
## 2. Bare stat grid → hero KPI with supporting stats
|
|
55
61
|
|
|
@@ -67,21 +73,28 @@ doing?") has no answer — every number claims the same importance.
|
|
|
67
73
|
```
|
|
68
74
|
|
|
69
75
|
```tsx
|
|
70
|
-
// AFTER — one hero
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
// AFTER — one hero as a Stat, the rest demoted to a footer strip
|
|
77
|
+
import {
|
|
78
|
+
Stat,
|
|
79
|
+
StatChange,
|
|
80
|
+
StatDetail,
|
|
81
|
+
StatFigure,
|
|
82
|
+
StatLabel,
|
|
83
|
+
StatValue,
|
|
84
|
+
} from '@ekanos/ui/stat';
|
|
85
|
+
|
|
86
|
+
<div className="px-5 py-5">
|
|
87
|
+
<Stat>
|
|
88
|
+
<StatLabel>Revenue this month</StatLabel>
|
|
89
|
+
<StatFigure>
|
|
90
|
+
<StatValue>{formatCurrency(revenue)}</StatValue>
|
|
91
|
+
<StatChange tone={delta >= 0 ? 'positive' : 'negative'}>
|
|
92
|
+
{delta >= 0 ? '+' : ''}
|
|
93
|
+
{delta.toFixed(1)}%
|
|
94
|
+
</StatChange>
|
|
95
|
+
</StatFigure>
|
|
96
|
+
<StatDetail>vs. last month</StatDetail>
|
|
97
|
+
</Stat>
|
|
85
98
|
</div>
|
|
86
99
|
|
|
87
100
|
<div className="flex shrink-0 items-center border-t">
|
|
@@ -93,59 +106,72 @@ doing?") has no answer — every number claims the same importance.
|
|
|
93
106
|
<Fragment key={stat.label}>
|
|
94
107
|
{i > 0 && <div className="bg-border h-8 w-px" />}
|
|
95
108
|
<div className="flex flex-1 flex-col items-center py-3">
|
|
96
|
-
<span className="text-
|
|
97
|
-
|
|
109
|
+
<span className="text-base font-semibold tabular-nums">
|
|
110
|
+
{stat.value}
|
|
111
|
+
</span>
|
|
112
|
+
<span className="text-muted-foreground text-sm">{stat.label}</span>
|
|
98
113
|
</div>
|
|
99
114
|
</Fragment>
|
|
100
115
|
))}
|
|
101
116
|
</div>
|
|
102
117
|
```
|
|
103
118
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
119
|
+
`Stat` owns the sizes — the heading face, `text-2xl`, `tracking-tight`,
|
|
120
|
+
`tabular-nums`, and the arrow glyph that pairs with `StatChange`'s color so
|
|
121
|
+
the tone never rides on hue alone. Don't override them, and don't rebuild
|
|
122
|
+
them by hand: a Source that swaps typefaces carries the component, not your
|
|
123
|
+
spans.
|
|
124
|
+
|
|
125
|
+
Pick the hero by the interview's SEE-outcome, not by which number is largest.
|
|
126
|
+
One `Stat` per widget is the hero; a row of up to three `Stat`s is fine —
|
|
127
|
+
they are sized not to compete. A second `StatValue`-sized figure in the body
|
|
128
|
+
is two heroes, which is zero heroes.
|
|
107
129
|
|
|
108
|
-
## 3. Unstyled status →
|
|
130
|
+
## 3. Unstyled status → `StatusBadge`
|
|
109
131
|
|
|
110
|
-
**Failure:** status as bare text,
|
|
111
|
-
breaks in dark mode and off-brand tenants
|
|
132
|
+
**Failure:** status as bare text, a hardcoded palette (`bg-green-500`) that
|
|
133
|
+
breaks in dark mode and off-brand tenants, or a tone passed to `Badge`'s
|
|
134
|
+
`variant` — which no longer exists.
|
|
112
135
|
|
|
113
136
|
```tsx
|
|
114
137
|
// BEFORE
|
|
115
138
|
<span className="text-xs">{sub.status}</span>
|
|
116
139
|
// or
|
|
117
140
|
<span className="rounded bg-green-500 px-1 text-white">{sub.status}</span>
|
|
141
|
+
// or, just as wrong today:
|
|
142
|
+
<Badge variant="success">Active</Badge>
|
|
118
143
|
```
|
|
119
144
|
|
|
120
145
|
```tsx
|
|
121
|
-
// AFTER —
|
|
122
|
-
import {
|
|
146
|
+
// AFTER — StatusBadge carries the tone; the map is the single source
|
|
147
|
+
import { StatusBadge } from '@ekanos/ui/status-badge';
|
|
123
148
|
|
|
124
|
-
const
|
|
149
|
+
const STATUS_TONE = {
|
|
125
150
|
active: 'success',
|
|
126
151
|
trialing: 'info',
|
|
127
152
|
past_due: 'warning',
|
|
128
153
|
canceled: 'destructive',
|
|
129
154
|
} as const;
|
|
130
155
|
|
|
131
|
-
<
|
|
156
|
+
<StatusBadge tone={STATUS_TONE[sub.status] ?? 'neutral'}>
|
|
132
157
|
{STATUS_LABEL[sub.status] ?? sub.status}
|
|
133
|
-
</
|
|
158
|
+
</StatusBadge>;
|
|
134
159
|
```
|
|
135
160
|
|
|
136
|
-
|
|
161
|
+
`StatusBadge` renders a neutral outlined pill with a `size-2` dot in the
|
|
162
|
+
tone: the words carry the meaning and the dot confirms it, so a long list
|
|
163
|
+
stays calm and a color-blind reader loses nothing. That is why it works in a
|
|
164
|
+
dense list and a tinted pill does not — there is no quieter variant to reach
|
|
165
|
+
for.
|
|
137
166
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
/>
|
|
145
|
-
<span className="text-muted-foreground text-sm">{STATUS_LABEL[sub.status]}</span>
|
|
146
|
-
```
|
|
167
|
+
`Badge` is for counts, categories and tags only. **It has no tone variants**:
|
|
168
|
+
`success`, `warning` and `info` were deleted from the host kit on 2026-10-06.
|
|
169
|
+
`@ekanos/ui`'s `Badge` still accepts one, for integrations published before
|
|
170
|
+
that date, and silently renders the `StatusBadge` pill instead — so writing
|
|
171
|
+
one today is dead code that ships a pill you did not ask for, and the host's
|
|
172
|
+
own lint rejects it outright.
|
|
147
173
|
|
|
148
|
-
One status treatment per row. A row with a colored
|
|
174
|
+
One status treatment per row. A row with a colored pill AND a colored icon
|
|
149
175
|
AND colored money is signalling nothing.
|
|
150
176
|
|
|
151
177
|
## 4. Cramped card → sizing floors applied
|
|
@@ -166,10 +192,13 @@ is not information-rich; it is unreadable.
|
|
|
166
192
|
```
|
|
167
193
|
|
|
168
194
|
```tsx
|
|
169
|
-
// AFTER — floors: rows px-
|
|
170
|
-
<div className="divide-border divide-y">
|
|
195
|
+
// AFTER — floors: rows px-5 py-3, icons h-5 w-5, body text-base
|
|
196
|
+
<div className="divide-border/50 divide-y">
|
|
171
197
|
{events.slice(0, 6).map((e) => (
|
|
172
|
-
<div
|
|
198
|
+
<div
|
|
199
|
+
key={e.id}
|
|
200
|
+
className="hover:bg-accent/50 flex items-start gap-3 px-5 py-3"
|
|
201
|
+
>
|
|
173
202
|
<span className="bg-muted text-muted-foreground shrink-0 rounded-full p-2">
|
|
174
203
|
<Icon name={eventIcon(e.type)} className="h-5 w-5" />
|
|
175
204
|
</span>
|
|
@@ -207,7 +236,7 @@ import { Button } from '@ekanos/ui/button';
|
|
|
207
236
|
<div className="divide-border divide-y">{rows}</div>
|
|
208
237
|
</div>
|
|
209
238
|
{items.length > 7 && (
|
|
210
|
-
<div className="shrink-0 border-t px-
|
|
239
|
+
<div className="shrink-0 border-t px-5 py-2">
|
|
211
240
|
<Button variant="ghost" size="sm" className="text-muted-foreground">
|
|
212
241
|
View all {items.length}
|
|
213
242
|
</Button>
|
|
@@ -252,22 +281,25 @@ import {
|
|
|
252
281
|
The one visible action should be the urgent one; if no action is urgent, all
|
|
253
282
|
of them belong in the menu.
|
|
254
283
|
|
|
255
|
-
## 7. Blank empty state →
|
|
284
|
+
## 7. Blank empty state → `Widget.Empty`
|
|
256
285
|
|
|
257
286
|
**Failure:** an empty array renders an empty card, indistinguishable from a
|
|
258
287
|
bug. Empty is a state you design, distinct from error.
|
|
259
288
|
|
|
289
|
+
The SDK owns this state — don't hand-roll it. `Widget.Empty` is positive by
|
|
290
|
+
default ("You're all caught up") and neutral only when nothing good can be
|
|
291
|
+
said, and it matches every other widget on the board:
|
|
292
|
+
|
|
260
293
|
```tsx
|
|
261
|
-
// AFTER —
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
<p className="text-muted-foreground max-w-xs text-sm">
|
|
268
|
-
Invoices appear here as soon as your first one is issued.
|
|
269
|
-
</p>
|
|
270
|
-
</div>
|
|
294
|
+
// AFTER — the SDK's empty state, one sentence of what appears here
|
|
295
|
+
<Widget.Empty
|
|
296
|
+
icon="fa-light fa-inbox"
|
|
297
|
+
title="No invoices yet"
|
|
298
|
+
description="Invoices appear here as soon as your first one is issued."
|
|
299
|
+
/>
|
|
271
300
|
```
|
|
272
301
|
|
|
273
|
-
|
|
302
|
+
Check the SDK README §`components.widgets` for the exact prop names in your
|
|
303
|
+
installed version. No CTA unless there is a real action the user can take
|
|
304
|
+
from the dashboard — and "not connected" is a different state again
|
|
305
|
+
(`Widget.Inactive reason="not-connected"`), never a lock screen.
|
|
@@ -10,16 +10,60 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { createElement } from 'react';
|
|
12
12
|
|
|
13
|
+
import { Widget } from '@ekanos/sdk/components';
|
|
13
14
|
import { defineIntegration } from '@ekanos/sdk/integration';
|
|
15
|
+
import {
|
|
16
|
+
Stat,
|
|
17
|
+
StatDetail,
|
|
18
|
+
StatFigure,
|
|
19
|
+
StatLabel,
|
|
20
|
+
StatValue,
|
|
21
|
+
} from '@ekanos/ui/stat';
|
|
14
22
|
|
|
15
|
-
// A minimal dashboard widget
|
|
16
|
-
//
|
|
17
|
-
//
|
|
23
|
+
// A minimal dashboard widget, built from the Widget.* compound API and the
|
|
24
|
+
// host's own `Stat`. Two rules this starter encodes, because they are the two
|
|
25
|
+
// partners get wrong most often:
|
|
26
|
+
//
|
|
27
|
+
// * Never a hand-sized figure. `Stat` sets the label, the figure and the
|
|
28
|
+
// context line to the host's spec, so a Source that swaps typefaces or
|
|
29
|
+
// corners carries your widget with it.
|
|
30
|
+
// * Never an inline `style`. Everything is a Tailwind class on the host's
|
|
31
|
+
// semantic tokens; an inline style escapes the theme, dark mode and the
|
|
32
|
+
// Source's branding, and `ekanos validate` warns on it.
|
|
33
|
+
//
|
|
34
|
+
// This starter uses React.createElement so it can live in a .ts file. Move it
|
|
35
|
+
// to its own .tsx file as soon as it grows past this — `.claude/skills/
|
|
36
|
+
// build-integration/references/widget-design.md` is the design guide.
|
|
18
37
|
function HelloWidget() {
|
|
19
38
|
return createElement(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
Widget.DataState,
|
|
40
|
+
null,
|
|
41
|
+
createElement(
|
|
42
|
+
Widget.Card,
|
|
43
|
+
null,
|
|
44
|
+
createElement(Widget.Header, { context: 'Since you scaffolded this' }),
|
|
45
|
+
createElement(
|
|
46
|
+
Widget.Active,
|
|
47
|
+
null,
|
|
48
|
+
createElement(
|
|
49
|
+
Widget.Body,
|
|
50
|
+
null,
|
|
51
|
+
createElement(
|
|
52
|
+
Stat,
|
|
53
|
+
null,
|
|
54
|
+
createElement(StatLabel, null, 'Greetings sent'),
|
|
55
|
+
createElement(
|
|
56
|
+
StatFigure,
|
|
57
|
+
null,
|
|
58
|
+
createElement(StatValue, null, '1'),
|
|
59
|
+
),
|
|
60
|
+
createElement(StatDetail, null, 'Hello from __DISPLAY_NAME__'),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
createElement(Widget.Loading, { variant: 'stat' }),
|
|
65
|
+
createElement(Widget.Error, null),
|
|
66
|
+
),
|
|
23
67
|
);
|
|
24
68
|
}
|
|
25
69
|
|