@ekanos/cli 0.1.6 → 0.1.8

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.
Files changed (47) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/init.d.ts +36 -0
  3. package/dist/commands/init.js +126 -10
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/publish.d.ts +5 -0
  6. package/dist/commands/publish.js +1 -1
  7. package/dist/commands/publish.js.map +1 -1
  8. package/dist/commands/status.d.ts +27 -1
  9. package/dist/commands/status.js +145 -12
  10. package/dist/commands/status.js.map +1 -1
  11. package/dist/commands/upgrade.js +108 -7
  12. package/dist/commands/upgrade.js.map +1 -1
  13. package/dist/copy-tree.d.ts +48 -0
  14. package/dist/copy-tree.js +55 -0
  15. package/dist/copy-tree.js.map +1 -0
  16. package/dist/index.d.ts +6 -0
  17. package/dist/index.js +12 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/parse-argv.d.ts +7 -1
  20. package/dist/parse-argv.js +10 -2
  21. package/dist/parse-argv.js.map +1 -1
  22. package/dist/project-checks.d.ts +9 -0
  23. package/dist/project-checks.js +29 -0
  24. package/dist/project-checks.js.map +1 -1
  25. package/dist/project.d.ts +23 -0
  26. package/dist/project.js +66 -1
  27. package/dist/project.js.map +1 -1
  28. package/dist/publish-api.d.ts +48 -4
  29. package/dist/publish-api.js +32 -13
  30. package/dist/publish-api.js.map +1 -1
  31. package/dist/safe-fs.d.ts +1 -1
  32. package/dist/safe-fs.js.map +1 -1
  33. package/dist/sources-api.d.ts +1 -1
  34. package/dist/sources-api.js.map +1 -1
  35. package/dist/validate-findings.js +2 -1
  36. package/dist/validate-findings.js.map +1 -1
  37. package/package.json +1 -1
  38. package/templates/AGENTS.md.tmpl +57 -0
  39. package/templates/claude-skill.md.tmpl +7 -0
  40. package/templates/docs/api-notes.md.tmpl +21 -0
  41. package/templates/mcp.json.tmpl +8 -0
  42. package/templates/skills/build-integration/SKILL.md +178 -0
  43. package/templates/skills/build-integration/references/api-docs-research.md +93 -0
  44. package/templates/skills/build-integration/references/api-wiring.md +94 -0
  45. package/templates/skills/build-integration/references/widget-design.md +132 -0
  46. package/templates/skills/polish-widgets/SKILL.md +139 -0
  47. package/templates/skills/polish-widgets/references/patterns.md +269 -0
@@ -0,0 +1,93 @@
1
+ # Researching the target API
2
+
3
+ Read this at Phase 2 (Research), before fetching anything. The ladder and the
4
+ hard rule live in SKILL.md; this file is the mechanics — how to work each
5
+ rung and what to leave behind in `docs/api-notes.md`.
6
+
7
+ ## Reading an OpenAPI/Swagger spec efficiently
8
+
9
+ Do not ingest the whole spec — production specs run to megabytes and most of
10
+ it is endpoints the plan will never touch. Read in this order:
11
+
12
+ 1. `servers` (or Swagger 2.0 `host` + `basePath`) — the real base URL(s),
13
+ including environment variants (sandbox vs production).
14
+ 2. `components.securitySchemes` (2.0: `securityDefinitions`) plus top-level
15
+ `security` — the auth model and the EXACT header/query shape. This settles
16
+ the interview's "not sure" auth answer.
17
+ 3. Only the paths the plan needs: search the spec for the resource nouns from
18
+ the plan's outcomes, then read those operations fully — method, path
19
+ template, required params, request/response schemas, error responses.
20
+ 4. Pagination: look at the list operations' params (`cursor`, `page`,
21
+ `limit`, `starting_after`) and response envelopes (`next`, `has_more`,
22
+ `Link` header) — pagination is per-operation, never assume one scheme.
23
+
24
+ If the spec is JSON, `jq` the sections above rather than reading linearly.
25
+ Note the spec's own `info.version` and the URL it was fetched from in the
26
+ notes — that is the line future sessions use to detect drift.
27
+
28
+ ## Extracting from prose docs
29
+
30
+ Capture: base URL, auth header shape (copy a verbatim example request),
31
+ endpoint tables (method, path, required params), pagination scheme, documented
32
+ rate limits and the 429 behavior, error response body shape, webhook event
33
+ names and signature scheme. Copy exact strings — paths, header names, event
34
+ names — never paraphrase them.
35
+
36
+ Skip: marketing pages, quickstart tutorials, SDK installation guides,
37
+ changelog prose. Watch for versioned base URLs (`/v1/` vs `/v2/`) — docs
38
+ sites routinely show both, and mixing them is a classic silent 404.
39
+
40
+ ## Using Context7
41
+
42
+ Context7 (via this project's `.mcp.json`) serves indexed documentation for
43
+ libraries and SDKs. It is good for: the target service's OFFICIAL client
44
+ library — method signatures, initialization, idiomatic usage — and for
45
+ questions about packages the integration depends on. It is NOT an authority
46
+ on raw REST surfaces: when Context7's account of an endpoint disagrees with
47
+ the OpenAPI spec or official docs, the spec wins. If the integration calls
48
+ the REST API directly (the common case — `ctx.fetch` means no vendor SDK),
49
+ Context7 is a supplement, not a source.
50
+
51
+ ## The `docs/api-notes.md` structure
52
+
53
+ The scaffold ships a stub; grow it into this shape (drop sections that do not
54
+ apply, keep the order):
55
+
56
+ # API notes: <service>
57
+ Sources: <spec/docs URLs + fetch date>
58
+
59
+ ## Auth
60
+ <scheme; exact header shape, e.g. `Authorization: Bearer <key>`>
61
+
62
+ ## Base URLs
63
+ <production; sandbox if any>
64
+
65
+ ## Endpoints
66
+ | Outcome | Method | Path | Key params |
67
+ |---|---|---|---|
68
+ | open invoices widget | GET | /v1/invoices | status, limit, cursor |
69
+
70
+ ## Pagination
71
+ <scheme, per-operation exceptions>
72
+
73
+ ## Rate limits
74
+ <documented limits; 429 behavior, Retry-After>
75
+
76
+ ## Errors
77
+ <error body shape; which statuses are retryable>
78
+
79
+ ## Webhooks
80
+ <events used, signature header + scheme, retry behavior>
81
+
82
+ Every endpoint the Wire phase touches must have a row here first. Keep it a
83
+ snapshot, not a mirror — only what THIS integration uses.
84
+
85
+ ## When the live API contradicts the notes
86
+
87
+ A 404 on a noted path, an auth header that bounces, a response missing a
88
+ noted field: believe the live API. Update `docs/api-notes.md` in the SAME
89
+ commit as the code fix — a note that disagrees with shipped code is worse
90
+ than no note — and record what was observed and when, so the correction is
91
+ not "fixed back" from the stale docs site later. If the contradiction changes
92
+ the plan (an endpoint gone, auth scheme different), surface it to the user
93
+ before wiring around it.
@@ -0,0 +1,94 @@
1
+ # Wiring an integration to its target API
2
+
3
+ Read this at Phase 5 (Wire). The binding contract (ctx surface, storage
4
+ vocabulary, secret naming) is `AGENTS.md`; the target API's contract is
5
+ `docs/api-notes.md` — write client code from the notes, not from memory, and
6
+ when the live API contradicts them, fix the notes in the same commit as the
7
+ code. This file is the order of work and the traps.
8
+
9
+ ## Order of work
10
+
11
+ 1. **Constants first** — `src/config.ts` exporting every name that crosses a
12
+ boundary: secret names (= activation field names), storage keys, webhook
13
+ ids, egress origins. Both the form and every handler import from here;
14
+ a renamed form field silently orphans its secret otherwise.
15
+ 2. **Activation form** — zod schema + react-hook-form inside
16
+ `BaseActivationForm` (see AGENTS.md). Every string field the user submits
17
+ becomes `ctx.secrets.get('<field name>')`. Validate the credential shape
18
+ in the schema (prefix, length) so a paste error fails at the form, not in
19
+ a handler.
20
+ 3. **A thin API client** — one module owning base URL, auth header assembly,
21
+ response parsing, and error mapping, taking `ctx` (or just
22
+ `{ fetch, secrets }`) as an argument. Handlers never call `ctx.fetch`
23
+ directly for endpoints the client covers.
24
+ 4. **Handlers** — tools, then webhooks, then schedules, each with tests as
25
+ they are written (`createMockContext`, `invokeWebhook`, `invokeSchedule`
26
+ from `@ekanos/sdk/testing`).
27
+ 5. **Widget data** — widgets cannot call the target API (no client-side
28
+ `ctx`). The pattern: a schedule (or webhook) fetches and writes a
29
+ `clientReadable` storage key; the widget reads it back with
30
+ `fetchIntegrationStorage()` from `@ekanos/sdk/hooks`. Cache freshness in
31
+ the stored value (`fetchedAt`) and show it in the UI when staleness
32
+ matters.
33
+
34
+ ## Traps, each one earned
35
+
36
+ - **Egress is an allowlist, checked synchronously.** Every origin the client
37
+ touches must be in `egress` — including the OAuth token host and any host a
38
+ redirect lands on. A missing origin is a thrown error, not a warning.
39
+ - **Secrets never leave the secrets surface.** Not into `ctx.storage`, not
40
+ into logs, not into a tool result, not into a widget's stored value. Token
41
+ rotation: `ctx.secrets.set()` (account tier only).
42
+ - **`ctx.storage.user` throws for machine actors** (workflow runs). A tool
43
+ that touches user storage must branch on `ctx.actor.kind` or stick to
44
+ account storage.
45
+ - **Webhook signatures are declared, not hand-verified** — `signature:
46
+ { header, secretName }` in the declaration; the transport verifies before
47
+ the handler runs. Return `{ status: 'ignored' }` for valid-but-irrelevant
48
+ deliveries so the sender stops retrying; throw only for retryable faults.
49
+ - **Handlers run under redelivery and "Run now".** Make writes idempotent
50
+ (stable keys, upsert semantics); `invocation.trigger` distinguishes cron
51
+ from manual.
52
+ - **Schedules are 5-field numeric cron** — no names, no `@daily`.
53
+ - **Rate limits**: respect `Retry-After` on 429 in the client; a schedule
54
+ that hammers a free-tier API takes every tenant down with it.
55
+ - **Storage schema evolution**: a stored value that no longer parses throws
56
+ on read — version the key or widen with a union; never edit a schema to a
57
+ shape old rows can't satisfy.
58
+ - **Cache rows must know what they were built from.** A cache keyed by
59
+ nothing and invalidated only by time serves the PREVIOUS configuration
60
+ after the user changes activation data (old city, old account, old list)
61
+ until the next schedule tick — confidently wrong answers. Embed a config
62
+ fingerprint in the stored value (the activation fields the fetch depended
63
+ on) and treat a mismatch on read as a cache miss. Tools reading the cache
64
+ must check the fingerprint, not just row freshness. Three details that are
65
+ each easy to get wrong:
66
+ - Include the fields that change the VALUES, not just the ones that select
67
+ the data — units, currency, locale, timezone. They leave the identity
68
+ byte-identical while making every number wrong.
69
+ - Stabilize noisy members (round coordinates, normalize case) so the
70
+ fingerprint doesn't churn under inputs the answer doesn't depend on.
71
+ - Declare the fingerprint field `.optional()` and treat ABSENT as a miss.
72
+ A required field makes every pre-existing row throw on read (see the
73
+ schema-evolution trap above) — and for a `clientReadable` key that throw
74
+ reaches the widgets, turning a cache improvement into a dashboard
75
+ outage. Optional-plus-absent-is-stale degrades to a refetch instead.
76
+
77
+ ## Test floor per handler
78
+
79
+ - happy path against `createMockContext` with seeded secrets + programmed
80
+ fetch responses
81
+ - one failure path (API 500/timeout) proving the handler returns/throws the
82
+ designed way rather than leaking the raw error
83
+ - for webhooks: one `ignored` case; for schedules: one idempotency case
84
+ (same invocation twice, no duplicate writes)
85
+
86
+ ## Verifying against the real API — live mode
87
+
88
+ The harness mocks Fusion, never the vendor. Do not tell the developer that
89
+ local work is mock-only: opt the integration into live mode (a `live` block
90
+ on its `harness.config.ts` entry — see AGENTS.md "Live mode") and the
91
+ toolbar's fixtures ⇄ live switch runs real widget queries against the real
92
+ API, gated only by the declared `egress`. Use fixtures for the Fusion-side
93
+ state widgets need first, and for webhook/schedule handlers (the Triggers
94
+ surface runs those against the mock context by design).
@@ -0,0 +1,132 @@
1
+ # Widget design for Ekanos integrations
2
+
3
+ Read this before building the first widget. The mechanics (declaration shape,
4
+ `Widget.*` API, `WidgetContext`, preview) are in `AGENTS.md` and the
5
+ `@ekanos/sdk` README §`components.widgets` — this file is the design judgment.
6
+
7
+ ## Philosophy: form over function
8
+
9
+ Build the widget beautiful with realistic MOCK data first; wire real data
10
+ last. A widget that renders nothing interesting with perfect plumbing is a
11
+ failure; the reverse is a demo. Sequence per widget:
12
+
13
+ 1. Distinctive UI with hardcoded, realistic mock data
14
+ 2. All four states (loading / empty / error / success)
15
+ 3. Real data via the wiring reference
16
+ 4. A final styling pass against the checklist below
17
+
18
+ "Distinctive" means: if the mock data were swapped for another service's, the
19
+ widget should NOT still look right. Lead with the one number, chart, or list
20
+ the user said matters (the interview's SEE-outcome); trim everything that
21
+ does not serve it.
22
+
23
+ ## The key question — decide it before writing JSX
24
+
25
+ Every widget answers 1–2 questions, and the answer must be the most prominent
26
+ element on it — size, weight, position. Everything else is supporting context
27
+ and recedes (`text-muted-foreground`, smaller). Name the question from the
28
+ interview's SEE-outcome before writing the first class; a widget you cannot
29
+ name the question for is a widget you cannot design.
30
+
31
+ | Widget type | Key question | Most prominent element |
32
+ | ------------------- | ---------------------------------- | --------------------------------------------------------------------- |
33
+ | KPI / stat | "How are we doing?" | The hero number: `text-3xl font-semibold tracking-tight tabular-nums` |
34
+ | List / feed | "What needs my attention?" | Item name `text-base font-medium` + its value, right-aligned |
35
+ | Chart | "Which way is it going?" | The chart + current value; axes and legends muted |
36
+ | Action / connection | "What can I do / who's connected?" | One clear CTA or the identity + status |
37
+
38
+ The default failure is uniform weight: everything `text-sm`, values dressed
39
+ like labels. Squint at the render — if nothing reads first, there is no
40
+ hierarchy, and no amount of padding will save it.
41
+
42
+ ## Layout recipes (the shapes that work)
43
+
44
+ **KPI / stat card** — one hero, a delta, supporting stats demoted:
45
+
46
+ - Center block: label `text-sm text-muted-foreground`, then the hero value
47
+ `text-3xl font-semibold tracking-tight tabular-nums`, then a delta line
48
+ `text-sm font-medium` in `text-success-strong` / `text-destructive`.
49
+ - Supporting stats as a `border-t` footer strip: per-stat
50
+ `text-lg font-semibold` over a `text-xs text-muted-foreground` label,
51
+ columns separated by `bg-border h-8 w-px`.
52
+ - One `text-3xl` element per widget — two heroes is zero heroes.
53
+
54
+ **List / feed** — rows with a first read:
55
+
56
+ - Container `divide-y`; each row `flex items-center gap-3 px-4 py-3`.
57
+ - Name: `min-w-0 flex-1` wrapping `truncate text-base font-medium`, with an
58
+ optional `text-sm text-muted-foreground` second line.
59
+ - Value: `shrink-0 text-right text-base font-semibold tabular-nums`.
60
+ - 5–7 rows visible; the rest behind inner scroll or a "View all" ghost button.
61
+ - One status treatment per row: a `Badge` variant (`success` / `warning` /
62
+ `info` / `destructive`) or an `h-2.5 w-2.5` semantic dot — never both.
63
+
64
+ **Chart** — the picture is the point:
65
+
66
+ - Chart fills the body; current value as a `text-2xl font-semibold` readout
67
+ above or beside it, axes/gridlines/legend all muted.
68
+ - Series colors from chart tokens (`--chart-1`…), never hand-picked palette.
69
+
70
+ ## The skeleton
71
+
72
+ Every widget: `'use client'`, `Widget.DataState` wrapping `Widget.Card` →
73
+ `Widget.Header` + `Widget.Loading` + `Widget.Error` + `Widget.Active`. Gate
74
+ fetching on `ctx?.state.state === 'active'` from `WidgetContext`. Copy the
75
+ canonical example from the SDK README rather than improvising the tree.
76
+
77
+ ## Four states, no exceptions
78
+
79
+ - **Loading**: `Widget.Loading` (or `skeleton` blocks shaped like the real
80
+ content — never a bare spinner in a big empty card).
81
+ - **Error**: `Widget.Error`, plus a one-line human cause when known. Never
82
+ render an exception message verbatim.
83
+ - **Empty**: a real designed state — icon (h-8 w-8, muted), one sentence
84
+ saying what will appear here and what action produces it. Empty ≠ error.
85
+ - **Success**: the design.
86
+
87
+ While iterating, expose a temporary way to flip states (a dev-only prop or a
88
+ mock switch) so the harness can show all four; remove it before publish.
89
+
90
+ ## Fusion design rules (the harness applies the host theme)
91
+
92
+ - **Semantic tokens only**: `bg-card`, `text-foreground`, `text-muted-foreground`,
93
+ `border-border`, `bg-primary`, `text-destructive`. NEVER raw palette
94
+ classes (`bg-white`, `text-gray-500`, `bg-amber-400`) or hex/oklch
95
+ literals — they break in dark mode and off-brand tenants.
96
+ - **Accent discipline**: the primary (coral-orange) on ≤10% of the widget —
97
+ one CTA, one highlighted value, one active state. Semantic status colors
98
+ (success/warning/destructive) are not accents.
99
+ - **Depth via shadow, not borders** where a surface needs lift.
100
+ - **Sizing floors**: body text `text-sm`+; `text-xs` only for tertiary
101
+ metadata (timestamps, badge counts). Icons in lists `h-5 w-5`+. Card
102
+ padding `p-5`/`p-6`. Row padding: `px-4 py-3` (the recipes above use
103
+ this; hard floor `py-2 px-3` — never below). Section gaps `space-y-5`+.
104
+ - **Numbers**: hero/KPI values large and `tracking-tight` with
105
+ `tabular-nums` when digits align.
106
+ - **Overflow**: the widget must hold its grid size — inner scroll
107
+ (`overflow-y-auto` on the list, never the card) and `truncate` on
108
+ user-supplied strings. Test the sm layout, not just lg.
109
+ - **Components**: build from `@ekanos/ui/*` (the module list is in
110
+ AGENTS.md); `cn()` from `@ekanos/ui/utils` for conditional classes; icons
111
+ through `@ekanos/ui/icon` (Font Awesome names — they render empty outside
112
+ a host that loads FA; that is expected in bare tests).
113
+
114
+ ## Layout sizing
115
+
116
+ Pick `layouts` from the content, not habit: KPI/stat cards 3–4 wide × 6–8
117
+ high; lists and feeds 4–6 × 10–14; charts 6–8 × 10–12 (grid units, lg
118
+ breakpoint). Give md/sm the same content priority order — cut columns, not
119
+ meaning.
120
+
121
+ ## Checklist before calling a widget done
122
+
123
+ - [ ] Four states render in the harness; user has seen them
124
+ - [ ] No raw color literals; accent ≤10%; dark mode looks intentional
125
+ - [ ] Text floors respected; nothing truncates that matters at sm
126
+ - [ ] Mock data replaced by real data (or the mock is clearly temporary and
127
+ the wiring phase is next)
128
+ - [ ] Widget id is slug-prefixed and globally unique
129
+
130
+ A styling review of existing widgets — worst-first findings, before→after
131
+ fixes, the tile — is the `polish-widgets` skill; use it rather than
132
+ re-deriving a review pass from this file.
@@ -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.