@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.
@@ -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 key={inv.id} className="flex items-center gap-3 px-4 py-3">
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-4 py-3`), the container owns none.
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, a delta, the rest demoted to a footer strip
71
- <div className="flex flex-1 flex-col items-center justify-center px-4 py-6">
72
- <span className="text-muted-foreground text-sm">Revenue this month</span>
73
- <span className="text-3xl font-semibold tracking-tight tabular-nums">
74
- {formatCurrency(revenue)}
75
- </span>
76
- <span
77
- className={cn(
78
- 'mt-1 text-sm font-medium',
79
- delta >= 0 ? 'text-success-strong' : 'text-destructive',
80
- )}
81
- >
82
- {delta >= 0 ? '+' : ''}
83
- {delta.toFixed(1)}% vs last month
84
- </span>
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-lg font-semibold tabular-nums">{stat.value}</span>
97
- <span className="text-muted-foreground text-xs">{stat.label}</span>
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
- (`cn` from `@ekanos/ui/utils`.) Pick the hero by the interview's SEE-outcome,
105
- not by which number is largest. One `text-3xl` element per widget — two heroes
106
- is zero heroes.
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 → semantic badge or dot
130
+ ## 3. Unstyled status → `StatusBadge`
109
131
 
110
- **Failure:** status as bare text, or hardcoded palette (`bg-green-500`) that
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 — Badge variants carry the semantics; the map is the single source
122
- import { Badge } from '@ekanos/ui/badge';
146
+ // AFTER — StatusBadge carries the tone; the map is the single source
147
+ import { StatusBadge } from '@ekanos/ui/status-badge';
123
148
 
124
- const STATUS_VARIANT = {
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
- <Badge variant={STATUS_VARIANT[sub.status] ?? 'outline'}>
156
+ <StatusBadge tone={STATUS_TONE[sub.status] ?? 'neutral'}>
132
157
  {STATUS_LABEL[sub.status] ?? sub.status}
133
- </Badge>;
158
+ </StatusBadge>;
134
159
  ```
135
160
 
136
- When a badge is too loud (dense lists), use a dot plus muted text instead:
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
- ```tsx
139
- <span
140
- className={cn(
141
- 'h-2.5 w-2.5 shrink-0 rounded-full',
142
- sub.status === 'active' ? 'bg-success' : 'bg-muted-foreground/40',
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 badge AND a colored icon
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-4 py-3, icons h-5 w-5, body text-sm+
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 key={e.id} className="flex items-start gap-3 px-4 py-3">
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-4 py-2">
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 → designed 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 — icon, one sentence of what appears here, what action produces it
262
- <div className="flex flex-1 flex-col items-center justify-center gap-3 p-8 text-center">
263
- <div className="bg-muted flex h-16 w-16 items-center justify-center rounded-full">
264
- <Icon name="fa-light fa-inbox" className="text-muted-foreground h-8 w-8" />
265
- </div>
266
- <p className="text-base font-medium">No invoices yet</p>
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
- No CTA unless there is a real action the user can take from the dashboard.
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. This starter uses React.createElement so it can
16
- // live in a .ts file; move it to its own .tsx file and reach for the Widget.*
17
- // compound API from '@ekanos/sdk/components' when you build something real.
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
- 'div',
21
- { style: { padding: 16 } },
22
- 'Hello from __DISPLAY_NAME__',
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