@ekanos/harness 0.1.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +879 -0
  3. package/dist/app.d.ts +4 -0
  4. package/dist/app.js +11 -0
  5. package/dist/config.d.ts +18 -0
  6. package/dist/config.js +34 -0
  7. package/dist/internal/components/ask-assistant-bridge.d.ts +10 -0
  8. package/dist/internal/components/ask-assistant-bridge.js +50 -0
  9. package/dist/internal/components/dashboard-grid.d.ts +45 -0
  10. package/dist/internal/components/dashboard-grid.js +84 -0
  11. package/dist/internal/components/dev-toolbar.d.ts +15 -0
  12. package/dist/internal/components/dev-toolbar.js +155 -0
  13. package/dist/internal/components/harness-providers.d.ts +16 -0
  14. package/dist/internal/components/harness-providers.js +83 -0
  15. package/dist/internal/components/harness-widget-provider.d.ts +60 -0
  16. package/dist/internal/components/harness-widget-provider.js +84 -0
  17. package/dist/internal/components/i18n-provider.d.ts +9 -0
  18. package/dist/internal/components/i18n-provider.js +9 -0
  19. package/dist/internal/components/row-groups.d.ts +23 -0
  20. package/dist/internal/components/row-groups.js +37 -0
  21. package/dist/internal/components/surface-nav.d.ts +4 -0
  22. package/dist/internal/components/surface-nav.js +70 -0
  23. package/dist/internal/components/viewport-frame.d.ts +14 -0
  24. package/dist/internal/components/viewport-frame.js +27 -0
  25. package/dist/internal/components/widget-boundary.d.ts +25 -0
  26. package/dist/internal/components/widget-boundary.js +44 -0
  27. package/dist/internal/components/widget-surface.d.ts +20 -0
  28. package/dist/internal/components/widget-surface.js +76 -0
  29. package/dist/internal/lib/fonts.d.ts +2 -0
  30. package/dist/internal/lib/fonts.js +22 -0
  31. package/dist/internal/lib/harness-fetch-interceptor.d.ts +89 -0
  32. package/dist/internal/lib/harness-fetch-interceptor.js +101 -0
  33. package/dist/internal/lib/harness-live-fetch.d.ts +66 -0
  34. package/dist/internal/lib/harness-live-fetch.js +121 -0
  35. package/dist/internal/lib/harness-query-client.d.ts +43 -0
  36. package/dist/internal/lib/harness-query-client.js +103 -0
  37. package/dist/internal/lib/http-fixtures.d.ts +145 -0
  38. package/dist/internal/lib/http-fixtures.js +256 -0
  39. package/dist/internal/lib/i18n.d.ts +2 -0
  40. package/dist/internal/lib/i18n.js +17 -0
  41. package/dist/internal/lib/redact.d.ts +33 -0
  42. package/dist/internal/lib/redact.js +43 -0
  43. package/dist/internal/lib/toolbar-context.d.ts +59 -0
  44. package/dist/internal/lib/toolbar-context.js +124 -0
  45. package/dist/internal/registry-context.d.ts +27 -0
  46. package/dist/internal/registry-context.js +51 -0
  47. package/dist/internal/routes/activation-page.d.ts +33 -0
  48. package/dist/internal/routes/activation-page.js +242 -0
  49. package/dist/internal/routes/index-page.d.ts +13 -0
  50. package/dist/internal/routes/index-page.js +62 -0
  51. package/dist/internal/routes/integration-layout.d.ts +9 -0
  52. package/dist/internal/routes/integration-layout.js +84 -0
  53. package/dist/internal/routes/root-layout.d.ts +34 -0
  54. package/dist/internal/routes/root-layout.js +39 -0
  55. package/dist/internal/routes/single-widget-page.d.ts +6 -0
  56. package/dist/internal/routes/single-widget-page.js +30 -0
  57. package/dist/internal/routes/tile-page.d.ts +1 -0
  58. package/dist/internal/routes/tile-page.js +88 -0
  59. package/dist/internal/routes/triggers-page.d.ts +1 -0
  60. package/dist/internal/routes/triggers-page.js +386 -0
  61. package/dist/internal/routes/widgets-page.d.ts +1 -0
  62. package/dist/internal/routes/widgets-page.js +19 -0
  63. package/dist/internal/surfaces.d.ts +25 -0
  64. package/dist/internal/surfaces.js +29 -0
  65. package/dist/mocks/team-account-workspace.d.ts +78 -0
  66. package/dist/mocks/team-account-workspace.js +86 -0
  67. package/dist/registry.d.ts +418 -0
  68. package/dist/registry.js +82 -0
  69. package/dist/routes.d.ts +24 -0
  70. package/dist/routes.js +15 -0
  71. package/dist/styles.css +236 -0
  72. package/package.json +101 -0
@@ -0,0 +1,242 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { notFound } from "next/navigation";
5
+ import { Alert, AlertDescription, AlertTitle } from "@ekanos/ui/alert";
6
+ import { Badge } from "@ekanos/ui/badge";
7
+ import { Button } from "@ekanos/ui/button";
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ DialogTitle
14
+ } from "@ekanos/ui/dialog";
15
+ import { HARNESS_ACCOUNT_ID } from "../../registry.js";
16
+ import { hasSensitiveValues, redactSensitive } from "../lib/redact.js";
17
+ import { useHarnessIntegration } from "../registry-context.js";
18
+ function ActivationPage() {
19
+ const [flow, setFlow] = useState({ open: true, connected: false, result: null, revealed: false });
20
+ const integration = useHarnessIntegration();
21
+ if (!integration) notFound();
22
+ const ActivationForm = integration.activationForm;
23
+ const definition = integration.definition;
24
+ const capabilities = definition?.capabilities ?? [];
25
+ const permissions = definition?.permissions ?? [];
26
+ const summary = definition?.description ?? integration.description;
27
+ return /* @__PURE__ */ jsxs(
28
+ "main",
29
+ {
30
+ className: "mx-auto flex w-full max-w-3xl flex-col gap-6 px-6 py-8",
31
+ "data-test": "activation",
32
+ children: [
33
+ /* @__PURE__ */ jsxs(Alert, { children: [
34
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Submissions go nowhere" }),
35
+ /* @__PURE__ */ jsx(AlertDescription, { children: /* @__PURE__ */ jsxs("p", { children: [
36
+ "The harness has no backend. Submitting shows you the",
37
+ " ",
38
+ /* @__PURE__ */ jsx("code", { children: "ActivationResult" }),
39
+ " your form handed back instead of posting it \u2014 which is the part worth checking, since that payload is what the real activation action would persist. The dialog, the capability and permission sections and the connected state are the real ones, rendered from your ",
40
+ /* @__PURE__ */ jsx("code", { children: "defineIntegration()" }),
41
+ " ",
42
+ "declaration."
43
+ ] }) })
44
+ ] }),
45
+ /* @__PURE__ */ jsxs("div", { className: "bg-card flex flex-wrap items-center justify-between gap-3 rounded-lg border p-5", children: [
46
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
47
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: flow.connected ? "Connected" : "Not connected" }),
48
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: flow.connected ? "The dialog now shows the manage view, whose only action is Disconnect." : "The dialog shows the scope disclosure and your activation form." })
49
+ ] }),
50
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
51
+ /* @__PURE__ */ jsx(
52
+ Button,
53
+ {
54
+ variant: "outline",
55
+ size: "sm",
56
+ onClick: () => setFlow((prev) => ({ ...prev, open: true })),
57
+ "data-test": "activation-open-dialog",
58
+ children: "Open connect dialog"
59
+ }
60
+ ),
61
+ /* @__PURE__ */ jsx(
62
+ Button,
63
+ {
64
+ variant: "ghost",
65
+ size: "sm",
66
+ onClick: () => setFlow({
67
+ open: false,
68
+ connected: false,
69
+ result: null,
70
+ revealed: false
71
+ }),
72
+ children: "Reset flow"
73
+ }
74
+ )
75
+ ] })
76
+ ] }),
77
+ ActivationForm ? /* @__PURE__ */ jsx(
78
+ Dialog,
79
+ {
80
+ open: flow.open,
81
+ onOpenChange: (next) => setFlow((prev) => ({ ...prev, open: next })),
82
+ children: /* @__PURE__ */ jsxs(DialogContent, { "data-test": "integration-connect-dialog", children: [
83
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
84
+ /* @__PURE__ */ jsx("span", { className: "font-heading bg-muted text-muted-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-base font-semibold", children: integration.name.trim().charAt(0).toUpperCase() || "?" }),
85
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
86
+ /* @__PURE__ */ jsx(DialogTitle, { children: integration.name }),
87
+ /* @__PURE__ */ jsx(DialogDescription, { children: summary })
88
+ ] })
89
+ ] }) }),
90
+ /* @__PURE__ */ jsxs("div", { className: "flex max-h-[70vh] flex-col gap-5 overflow-y-auto", children: [
91
+ /* @__PURE__ */ jsx(CapabilityList, { capabilities }),
92
+ /* @__PURE__ */ jsx(PermissionList, { permissions }),
93
+ flow.connected ? /* @__PURE__ */ jsx(
94
+ ConnectedState,
95
+ {
96
+ name: integration.name,
97
+ onDisconnect: () => setFlow({
98
+ open: true,
99
+ connected: false,
100
+ result: null,
101
+ revealed: false
102
+ })
103
+ }
104
+ ) : (
105
+ /*
106
+ `inline` is REQUIRED. Without it `BaseActivationForm` renders
107
+ its own Dialog, which inside this one means nested dialogs —
108
+ exactly the constraint the real host comments on.
109
+ */
110
+ /* @__PURE__ */ jsx(
111
+ ActivationForm,
112
+ {
113
+ accountId: HARNESS_ACCOUNT_ID,
114
+ productSlug: integration.slug,
115
+ inline: true,
116
+ onSuccess: (result) => setFlow({
117
+ open: false,
118
+ connected: true,
119
+ result: result ?? null,
120
+ revealed: false
121
+ }),
122
+ onCancel: () => setFlow((prev) => ({ ...prev, open: false }))
123
+ }
124
+ )
125
+ )
126
+ ] })
127
+ ] })
128
+ }
129
+ ) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "This integration registers no activation form." }),
130
+ capabilities.length === 0 && permissions.length === 0 ? /* @__PURE__ */ jsxs(Alert, { children: [
131
+ /* @__PURE__ */ jsx(AlertTitle, { children: "No capabilities or permissions declared" }),
132
+ /* @__PURE__ */ jsx(AlertDescription, { children: /* @__PURE__ */ jsxs("p", { children: [
133
+ "The connect dialog renders \u201CWhat it does\u201D from",
134
+ " ",
135
+ /* @__PURE__ */ jsx("code", { children: "capabilities" }),
136
+ " and \u201CWhat it can access\u201D from ",
137
+ /* @__PURE__ */ jsx("code", { children: "permissions" }),
138
+ ". This integration declares neither, so a user consents to it with no idea what it reaches \u2014 add both to ",
139
+ /* @__PURE__ */ jsx("code", { children: "defineIntegration()" }),
140
+ "."
141
+ ] }) })
142
+ ] }) : null,
143
+ flow.result ? /* @__PURE__ */ jsxs("div", { className: "bg-card flex flex-col gap-3 rounded-lg border p-6", children: [
144
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
145
+ /* @__PURE__ */ jsx("h2", { className: "font-heading text-lg tracking-tight", children: "Submitted payload" }),
146
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
147
+ hasSensitiveValues(flow.result) ? /* @__PURE__ */ jsx(
148
+ Button,
149
+ {
150
+ variant: "ghost",
151
+ size: "sm",
152
+ onClick: () => setFlow((prev) => ({ ...prev, revealed: !prev.revealed })),
153
+ "data-test": "activation-reveal-payload",
154
+ children: flow.revealed ? "Hide values" : "Reveal values"
155
+ }
156
+ ) : null,
157
+ /* @__PURE__ */ jsx(
158
+ Button,
159
+ {
160
+ variant: "ghost",
161
+ size: "sm",
162
+ onClick: () => setFlow((prev) => ({
163
+ ...prev,
164
+ result: null,
165
+ revealed: false
166
+ })),
167
+ children: "Clear"
168
+ }
169
+ )
170
+ ] })
171
+ ] }),
172
+ /* @__PURE__ */ jsx("pre", { className: "bg-muted overflow-x-auto rounded-md p-3 text-xs", children: JSON.stringify(
173
+ flow.revealed ? flow.result : redactSensitive(flow.result),
174
+ null,
175
+ 2
176
+ ) })
177
+ ] }) : null
178
+ ]
179
+ }
180
+ );
181
+ }
182
+ function CapabilityList({
183
+ capabilities
184
+ }) {
185
+ if (capabilities.length === 0) return null;
186
+ return /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-2", children: [
187
+ /* @__PURE__ */ jsx("h3", { className: "text-muted-foreground text-xs font-semibold tracking-wider uppercase", children: "What it does" }),
188
+ /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-1.5", children: capabilities.map((capability) => /* @__PURE__ */ jsxs("li", { className: "text-foreground text-sm", children: [
189
+ capability.label,
190
+ capability.description ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
191
+ " ",
192
+ "\u2014 ",
193
+ capability.description
194
+ ] }) : null
195
+ ] }, capability.label)) })
196
+ ] });
197
+ }
198
+ function PermissionList({
199
+ permissions
200
+ }) {
201
+ if (permissions.length === 0) return null;
202
+ return /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-2", children: [
203
+ /* @__PURE__ */ jsx("h3", { className: "text-muted-foreground text-xs font-semibold tracking-wider uppercase", children: "What it can access" }),
204
+ /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-2", children: permissions.map((permission) => /* @__PURE__ */ jsxs("li", { className: "flex flex-col gap-0.5", children: [
205
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
206
+ /* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: permission.label }),
207
+ /* @__PURE__ */ jsx(
208
+ Badge,
209
+ {
210
+ variant: permission.type === "write" ? "warning" : "secondary",
211
+ children: permission.type === "write" ? "Write" : "Read"
212
+ }
213
+ )
214
+ ] }),
215
+ permission.detail ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: permission.detail }) : null
216
+ ] }, permission.label)) })
217
+ ] });
218
+ }
219
+ function ConnectedState({
220
+ name,
221
+ onDisconnect
222
+ }) {
223
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-5", children: [
224
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-sm", children: [
225
+ name,
226
+ " is connected to your workspace."
227
+ ] }),
228
+ /* @__PURE__ */ jsx(
229
+ Button,
230
+ {
231
+ variant: "outline",
232
+ className: "w-fit",
233
+ onClick: onDisconnect,
234
+ "data-test": "activation-disconnect",
235
+ children: "Disconnect"
236
+ }
237
+ )
238
+ ] });
239
+ }
240
+ export {
241
+ ActivationPage
242
+ };
@@ -0,0 +1,13 @@
1
+ import type { HarnessIntegration } from '../../registry.js';
2
+ /**
3
+ * The landing page: every registered integration, with a link to each of its
4
+ * surfaces.
5
+ *
6
+ * A server component, and worth keeping one — it reads nothing but the array
7
+ * it is handed. The registry travels as a prop rather than through
8
+ * `useHarnessRegistry()` precisely so this page never needs to be a client
9
+ * module; the provider below it is for the routes that do.
10
+ */
11
+ export declare function IndexPage({ integrations, }: {
12
+ integrations: HarnessIntegration[];
13
+ }): import("react").JSX.Element;
@@ -0,0 +1,62 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import Link from "next/link";
3
+ import { Icon } from "@ekanos/ui/icon";
4
+ import { HARNESS_SURFACES } from "../surfaces.js";
5
+ function IndexPage({
6
+ integrations
7
+ }) {
8
+ return /* @__PURE__ */ jsxs("main", { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-10", children: [
9
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-2", children: [
10
+ /* @__PURE__ */ jsx("h1", { className: "font-heading text-3xl tracking-tight", children: "Integration harness" }),
11
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Every registered integration, rendered in real Fusion chrome from fixtures. No Supabase, no auth, no network." })
12
+ ] }),
13
+ integrations.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "bg-card flex flex-col items-center gap-3 rounded-lg border p-6 text-center", children: [
14
+ /* @__PURE__ */ jsx(
15
+ Icon,
16
+ {
17
+ name: "fa-solid fa-box-open",
18
+ className: "text-muted-foreground h-6 w-6"
19
+ }
20
+ ),
21
+ /* @__PURE__ */ jsx("p", { className: "text-sm", children: "No integrations registered. Add one to your harness config at the project root." })
22
+ ] }) : /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-5", "data-test": "integration-list", children: integrations.map((integration) => /* @__PURE__ */ jsxs(
23
+ "li",
24
+ {
25
+ className: "bg-card shadow-card flex flex-col gap-5 rounded-lg p-6",
26
+ children: [
27
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
28
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-3", children: [
29
+ /* @__PURE__ */ jsx("h2", { className: "font-heading text-xl tracking-tight", children: integration.name }),
30
+ /* @__PURE__ */ jsx("code", { className: "text-muted-foreground text-xs", children: integration.slug })
31
+ ] }),
32
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: integration.description })
33
+ ] }),
34
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: HARNESS_SURFACES.map((surface) => /* @__PURE__ */ jsx(
35
+ Link,
36
+ {
37
+ href: `/${integration.slug}/${surface.segment}`,
38
+ className: "bg-accent/50 hover:bg-accent rounded-md px-3 py-2 text-sm transition-colors",
39
+ children: surface.shortLabel
40
+ },
41
+ surface.segment
42
+ )) }),
43
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-xs", children: [
44
+ integration.widgets.length,
45
+ " widget",
46
+ integration.widgets.length === 1 ? "" : "s",
47
+ " \xB7",
48
+ " ",
49
+ integration.tile ? "tile" : "no tile",
50
+ " \xB7",
51
+ " ",
52
+ integration.activationForm ? "activation form" : "no activation form"
53
+ ] })
54
+ ]
55
+ },
56
+ integration.slug
57
+ )) })
58
+ ] });
59
+ }
60
+ export {
61
+ IndexPage
62
+ };
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Client, not server: the registry holds React component references, which
4
+ * cannot cross the server/client boundary as props. Every surface below reads
5
+ * the injected registry out of context instead.
6
+ */
7
+ export declare function IntegrationLayout({ children }: {
8
+ children: ReactNode;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,84 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import { notFound } from "next/navigation";
5
+ import { Alert, AlertDescription, AlertTitle } from "@ekanos/ui/alert";
6
+ import {
7
+ findEgressMismatch,
8
+ resolveEgress,
9
+ seedsForMode,
10
+ supportsLiveMode
11
+ } from "../../registry.js";
12
+ import { HarnessProviders } from "../components/harness-providers.js";
13
+ import { SurfaceNav } from "../components/surface-nav.js";
14
+ import { compileFixtures, needsSelfOrigin } from "../lib/http-fixtures.js";
15
+ import { useToolbar } from "../lib/toolbar-context.js";
16
+ import { useHarnessIntegration } from "../registry-context.js";
17
+ function IntegrationLayout({ children }) {
18
+ const { state } = useToolbar();
19
+ const integration = useHarnessIntegration();
20
+ const egressMismatch = useMemo(
21
+ () => integration ? findEgressMismatch(integration) : null,
22
+ [integration]
23
+ );
24
+ const mode = supportsLiveMode(integration) && !egressMismatch ? state.dataMode : "fixtures";
25
+ const seeds = useMemo(
26
+ () => integration ? seedsForMode(integration, state.variant, mode) : [],
27
+ [integration, state.variant, mode]
28
+ );
29
+ const egress = useMemo(
30
+ () => integration ? resolveEgress(integration) : [],
31
+ [integration]
32
+ );
33
+ const httpFixtures = useMemo(() => {
34
+ if (!integration) return [];
35
+ const declared = integration.fixtures?.[state.variant] ?? [];
36
+ const selfOrigin = globalThis.location?.origin;
37
+ if (selfOrigin !== void 0) {
38
+ return compileFixtures(declared, egress, selfOrigin);
39
+ }
40
+ return compileFixtures(
41
+ declared.filter((fixture) => !needsSelfOrigin(fixture)),
42
+ egress
43
+ );
44
+ }, [integration, state.variant, egress]);
45
+ if (!integration) notFound();
46
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
47
+ /* @__PURE__ */ jsx(SurfaceNav, { integration }),
48
+ egressMismatch ? /* @__PURE__ */ jsx(EgressMismatchNotice, { mismatch: egressMismatch }) : null,
49
+ /* @__PURE__ */ jsx(
50
+ HarnessProviders,
51
+ {
52
+ seeds,
53
+ mode,
54
+ ...integration.live && !egressMismatch ? { live: integration.live } : {},
55
+ egress,
56
+ httpFixtures,
57
+ slug: integration.slug,
58
+ children
59
+ },
60
+ `${mode}-${state.variant}`
61
+ )
62
+ ] });
63
+ }
64
+ function EgressMismatchNotice({ mismatch }) {
65
+ return /* @__PURE__ */ jsx("div", { className: "px-6 pt-6", "data-test": "egress-mismatch", children: /* @__PURE__ */ jsxs(Alert, { variant: "destructive", children: [
66
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Live mode is disabled: `live.egress` declares origins your integration does not" }),
67
+ /* @__PURE__ */ jsxs(AlertDescription, { children: [
68
+ /* @__PURE__ */ jsxs("p", { children: [
69
+ "These origins are in your harness entry's",
70
+ " ",
71
+ /* @__PURE__ */ jsx("code", { children: "live.egress" }),
72
+ " but not in",
73
+ " ",
74
+ /* @__PURE__ */ jsx("code", { children: "defineIntegration({ egress })" }),
75
+ ":"
76
+ ] }),
77
+ /* @__PURE__ */ jsx("ul", { className: "mt-2 flex flex-col gap-1", children: mismatch.undeclared.map((origin) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("code", { className: "text-sm", children: origin }) }, origin)) }),
78
+ /* @__PURE__ */ jsx("p", { className: "mt-3", children: "The definition is what a reviewer reads and what production enforces, so running live against a wider list would prove nothing about the integration we actually ship. Import the same constant into both \u2014 never retype it \u2014 and this goes away." })
79
+ ] })
80
+ ] }) });
81
+ }
82
+ export {
83
+ IntegrationLayout
84
+ };
@@ -0,0 +1,34 @@
1
+ import '@fortawesome/fontawesome-free/css/all.min.css';
2
+ import type { HarnessIntegration } from '../../registry.js';
3
+ /**
4
+ * The `<html>`/`<body>` shell and every provider that has to sit above all
5
+ * routes.
6
+ *
7
+ * THE CLIENT BOUNDARY OF THE WHOLE HARNESS, and it has to be — the registry is
8
+ * the reason. A `HarnessIntegration` holds live React component references
9
+ * (`widgets[].component`, `tile`, `activationForm`) and, once a definition is
10
+ * linked, MCP tool `run` functions. Functions cannot be serialized across the
11
+ * server/client boundary, so a SERVER `RootLayout` handing this array to the
12
+ * client `HarnessRegistryProvider` fails the build outright:
13
+ *
14
+ * Functions cannot be passed directly to Client Components …
15
+ * {name: …, description: …, parameters: …, run: function run, …}
16
+ *
17
+ * The old app dodged this by having each client route import the registry
18
+ * module directly — the exact coupling this package exists to break. Injecting
19
+ * it instead means the boundary must sit ABOVE the injection point, which is
20
+ * here.
21
+ *
22
+ * Almost nothing is lost: every route, the toolbar and all four providers were
23
+ * already `'use client'`. `metadata` is unaffected — it is exported from the
24
+ * shell's own `app/layout.tsx`, which stays a server module, and `IndexPage`
25
+ * stays a server component because the shell's `app/page.tsx` renders it and
26
+ * passes the result down as `children`.
27
+ *
28
+ * `integrations` is the partner's registry, handed down from the shell's
29
+ * `app/layout.tsx`. Nothing in this package imports it.
30
+ */
31
+ export declare function RootLayout({ integrations, children, }: {
32
+ integrations: HarnessIntegration[];
33
+ children: React.ReactNode;
34
+ }): import("react").JSX.Element;
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { IconStyleOverrideProvider } from "@ekanos/ui/icon";
4
+ import { cn } from "@ekanos/ui/utils";
5
+ import "@fortawesome/fontawesome-free/css/all.min.css";
6
+ import { ThemeProvider } from "next-themes";
7
+ import { DevToolbar } from "../components/dev-toolbar.js";
8
+ import { I18nProvider } from "../components/i18n-provider.js";
9
+ import { fontVariables } from "../lib/fonts.js";
10
+ import { ToolbarProvider } from "../lib/toolbar-context.js";
11
+ import { HarnessRegistryProvider } from "../registry-context.js";
12
+ function RootLayout({
13
+ integrations,
14
+ children
15
+ }) {
16
+ return /* @__PURE__ */ jsx("html", { lang: "en", className: fontVariables, suppressHydrationWarning: true, children: /* @__PURE__ */ jsx(
17
+ "body",
18
+ {
19
+ className: cn("bg-background text-foreground min-h-screen antialiased"),
20
+ children: /* @__PURE__ */ jsx(IconStyleOverrideProvider, { value: "solid", children: /* @__PURE__ */ jsx(HarnessRegistryProvider, { integrations, children: /* @__PURE__ */ jsx(I18nProvider, { children: /* @__PURE__ */ jsx(
21
+ ThemeProvider,
22
+ {
23
+ attribute: "class",
24
+ defaultTheme: "light",
25
+ enableSystem: false,
26
+ enableColorScheme: false,
27
+ disableTransitionOnChange: true,
28
+ children: /* @__PURE__ */ jsxs(ToolbarProvider, { children: [
29
+ /* @__PURE__ */ jsx(DevToolbar, {}),
30
+ children
31
+ ] })
32
+ }
33
+ ) }) }) })
34
+ }
35
+ ) });
36
+ }
37
+ export {
38
+ RootLayout
39
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * One widget, full width, for focused iteration. Uses the same grid as the
3
+ * multi-widget page — a single `full`-width entry — so what you tune here is
4
+ * what you get over there.
5
+ */
6
+ export declare function SingleWidgetPage(): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { notFound } from "next/navigation";
4
+ import { findWidget } from "../../registry.js";
5
+ import { WidgetSurface } from "../components/widget-surface.js";
6
+ import { useHarnessIntegration, useHarnessWidgetId } from "../registry-context.js";
7
+ function SingleWidgetPage() {
8
+ const integration = useHarnessIntegration();
9
+ const widgetId = useHarnessWidgetId();
10
+ if (!integration) notFound();
11
+ const widget = findWidget(integration, widgetId);
12
+ if (!widget) notFound();
13
+ return /* @__PURE__ */ jsx(
14
+ "main",
15
+ {
16
+ className: "mx-auto w-full max-w-4xl px-6 py-8",
17
+ "data-test": "single-widget",
18
+ children: /* @__PURE__ */ jsx(
19
+ WidgetSurface,
20
+ {
21
+ widgets: [{ ...widget, width: "full" }],
22
+ productSlug: integration.slug
23
+ }
24
+ )
25
+ }
26
+ );
27
+ }
28
+ export {
29
+ SingleWidgetPage
30
+ };
@@ -0,0 +1 @@
1
+ export declare function TilePage(): import("react").JSX.Element;
@@ -0,0 +1,88 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { notFound } from "next/navigation";
4
+ import { Alert, AlertDescription, AlertTitle } from "@ekanos/ui/alert";
5
+ import { HARNESS_ACCOUNT_ID } from "../../registry.js";
6
+ import { ViewportFrame } from "../components/viewport-frame.js";
7
+ import { useHarnessIntegration } from "../registry-context.js";
8
+ const NEIGHBOURS = [
9
+ {
10
+ name: "Google Workspace",
11
+ description: "Mail, calendar and drive on the dashboard, and available to the assistant.",
12
+ vendor: "Google"
13
+ },
14
+ {
15
+ name: "HubSpot",
16
+ description: "Deals, contacts and pipeline stages, kept in step with your CRM.",
17
+ vendor: "HubSpot"
18
+ }
19
+ ];
20
+ function TilePage() {
21
+ const integration = useHarnessIntegration();
22
+ if (!integration) notFound();
23
+ const Tile = integration.tile;
24
+ return /* @__PURE__ */ jsxs(
25
+ "main",
26
+ {
27
+ className: "mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-8",
28
+ "data-test": "tile",
29
+ children: [
30
+ /* @__PURE__ */ jsxs(Alert, { children: [
31
+ /* @__PURE__ */ jsx(AlertTitle, { children: "The marketplace does not currently render this" }),
32
+ /* @__PURE__ */ jsx(AlertDescription, { children: /* @__PURE__ */ jsxs("p", { children: [
33
+ "22 of 23 shipped integrations return ",
34
+ /* @__PURE__ */ jsx("code", { children: "null" }),
35
+ " from",
36
+ " ",
37
+ /* @__PURE__ */ jsx("code", { children: "getMarketplaceTile()" }),
38
+ ", and the live marketplace route renders a generic product card that ignores the tile entirely. Treat this surface as a preview of a component that is, today, effectively unused \u2014 worth keeping correct, not worth investing heavily in."
39
+ ] }) })
40
+ ] }),
41
+ Tile ? /* @__PURE__ */ jsx(ViewportFrame, { children: /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-4", children: [
42
+ /* @__PURE__ */ jsxs("header", { children: [
43
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2.5", children: [
44
+ /* @__PURE__ */ jsx("h2", { className: "font-heading text-foreground text-xl font-semibold tracking-tight", children: "Available integrations" }),
45
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm tabular-nums", children: "3" })
46
+ ] }),
47
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground mt-1 max-w-prose text-sm", children: "Your tile in the catalogue grid, with two inert neighbours for scale. Column width, gap and heading come from the real marketplace." })
48
+ ] }),
49
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(17rem,1fr))] gap-4", children: [
50
+ /* @__PURE__ */ jsx(
51
+ Tile,
52
+ {
53
+ accountId: HARNESS_ACCOUNT_ID,
54
+ productSlug: integration.slug,
55
+ href: `/${integration.slug}/activation`
56
+ }
57
+ ),
58
+ NEIGHBOURS.map((neighbour) => /* @__PURE__ */ jsx(NeighbourCard, { ...neighbour }, neighbour.name))
59
+ ] })
60
+ ] }) }) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "This integration registers no marketplace tile." })
61
+ ]
62
+ }
63
+ );
64
+ }
65
+ function NeighbourCard({
66
+ name,
67
+ description,
68
+ vendor
69
+ }) {
70
+ return /* @__PURE__ */ jsxs(
71
+ "div",
72
+ {
73
+ "aria-hidden": "true",
74
+ className: "bg-card text-card-foreground shadow-card flex h-full flex-col gap-0 rounded-xl border p-5 opacity-60",
75
+ children: [
76
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
77
+ /* @__PURE__ */ jsx("span", { className: "font-heading bg-muted text-muted-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-base font-semibold", children: name.charAt(0) }),
78
+ /* @__PURE__ */ jsx("span", { className: "font-heading truncate text-base font-semibold tracking-tight", children: name })
79
+ ] }),
80
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground mt-3 line-clamp-2 text-sm", children: description }),
81
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground mt-auto pt-4 text-sm", children: vendor })
82
+ ]
83
+ }
84
+ );
85
+ }
86
+ export {
87
+ TilePage
88
+ };
@@ -0,0 +1 @@
1
+ export declare function TriggersPage(): import("react").JSX.Element;