@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
package/dist/app.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { Metadata } from 'next';
2
+ export { RootLayout } from './internal/routes/root-layout.js';
3
+ export { IndexPage } from './internal/routes/index-page.js';
4
+ export declare const harnessMetadata: Metadata;
package/dist/app.js ADDED
@@ -0,0 +1,11 @@
1
+ import { RootLayout } from "./internal/routes/root-layout.js";
2
+ import { IndexPage } from "./internal/routes/index-page.js";
3
+ const harnessMetadata = {
4
+ title: "Fusion Integration Harness",
5
+ description: "Render integration widgets, marketplace tiles and activation forms in real Fusion chrome, from fixtures, with no Supabase, auth or network."
6
+ };
7
+ export {
8
+ IndexPage,
9
+ RootLayout,
10
+ harnessMetadata
11
+ };
@@ -0,0 +1,18 @@
1
+ import type { HarnessIntegration } from './registry.js';
2
+ /**
3
+ * Identity function over the harness registry.
4
+ *
5
+ * It exists for the type inference, not for the runtime: wrapping the array in
6
+ * the partner's config module gets them completions and errors on every field
7
+ * of every entry — widget ids, fixture query keys, the `live` block — at the
8
+ * point they are typing them, rather than a wall of red on the array's closing
9
+ * bracket, which is what a bare `: HarnessIntegration[]` annotation produces.
10
+ *
11
+ * It also performs the few checks that TypeScript cannot: that slugs and widget
12
+ * ids are usable as URL segments, and that neither collides. Those are the
13
+ * mistakes that produce a blank page or a wrong page rather than an error, and
14
+ * this is the one place every registry entry passes through. Nothing else is
15
+ * validated or normalised — a partner can still fall back to a plain annotated
16
+ * array, and gets identical behaviour minus these checks.
17
+ */
18
+ export declare function defineHarnessConfig(integrations: HarnessIntegration[]): HarnessIntegration[];
package/dist/config.js ADDED
@@ -0,0 +1,34 @@
1
+ const SAFE_SEGMENT = /^[a-z0-9][a-z0-9._-]*$/i;
2
+ function assertSafeSegment(kind, value, where) {
3
+ if (SAFE_SEGMENT.test(value)) return;
4
+ throw new Error(
5
+ `defineHarnessConfig(): ${kind} ${JSON.stringify(value)} (${where}) is not usable as a URL segment. It must start with a letter or digit and contain only letters, digits, dot, underscore or hyphen \u2014 the harness routes every surface at /<slug>/\u2026, so a slash, space or "#" here silently produces a page that 404s.`
6
+ );
7
+ }
8
+ function defineHarnessConfig(integrations) {
9
+ const seenSlugs = /* @__PURE__ */ new Map();
10
+ integrations.forEach((integration, index) => {
11
+ assertSafeSegment("slug", integration.slug, `entry ${index}`);
12
+ const previous = seenSlugs.get(integration.slug);
13
+ if (previous !== void 0) {
14
+ throw new Error(
15
+ `defineHarnessConfig(): duplicate slug ${JSON.stringify(integration.slug)} at entries ${previous} and ${index}. Every surface is routed by slug, so the later entry would be permanently unreachable and the two would collide as React keys on the index page.`
16
+ );
17
+ }
18
+ seenSlugs.set(integration.slug, index);
19
+ const seenWidgets = /* @__PURE__ */ new Set();
20
+ for (const widget of integration.widgets) {
21
+ assertSafeSegment("widget id", widget.id, `slug "${integration.slug}"`);
22
+ if (seenWidgets.has(widget.id)) {
23
+ throw new Error(
24
+ `defineHarnessConfig(): duplicate widget id ${JSON.stringify(widget.id)} in "${integration.slug}". Widget ids address the single-widget surface, so the second one could never be opened.`
25
+ );
26
+ }
27
+ seenWidgets.add(widget.id);
28
+ }
29
+ });
30
+ return integrations;
31
+ }
32
+ export {
33
+ defineHarnessConfig
34
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Stands in for the AI assistant dock.
3
+ *
4
+ * In the real app, clicking a widget's "Ask about this" footer sends the
5
+ * widget's snapshot to the assistant. There is no assistant here — so instead
6
+ * the harness shows you the exact payload that would have been sent. That makes
7
+ * this panel the fastest way to check the snapshot rules: is the
8
+ * data faithful to what's on screen, are the filters included, did it recompute.
9
+ */
10
+ export declare function AskAssistantBridge(): import("react").JSX.Element | null;
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useState } from "react";
4
+ import { useRegisterAskAssistant } from "@ekanos/sdk/hooks";
5
+ import { Button } from "@ekanos/ui/button";
6
+ import { Icon } from "@ekanos/ui/icon";
7
+ function AskAssistantBridge() {
8
+ const [asked, setAsked] = useState(null);
9
+ const handler = useCallback((ctx) => {
10
+ setAsked(ctx);
11
+ }, []);
12
+ useRegisterAskAssistant(handler);
13
+ if (!asked) return null;
14
+ return /* @__PURE__ */ jsxs("div", { className: "fixed right-6 bottom-24 z-50 flex max-h-[60vh] w-[min(28rem,calc(100vw-3rem))] flex-col overflow-hidden rounded-lg border", children: [
15
+ /* @__PURE__ */ jsxs("div", { className: "bg-card flex items-center gap-3 border-b px-5 py-3", children: [
16
+ /* @__PURE__ */ jsx(
17
+ Icon,
18
+ {
19
+ name: "fa-solid fa-wand-magic-sparkles",
20
+ className: "text-primary h-5 w-5"
21
+ }
22
+ ),
23
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
24
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: "Ask payload" }),
25
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground truncate text-xs", children: [
26
+ asked.title,
27
+ " \xB7 ",
28
+ asked.widgetId
29
+ ] })
30
+ ] }),
31
+ /* @__PURE__ */ jsx(
32
+ Button,
33
+ {
34
+ variant: "ghost",
35
+ size: "icon",
36
+ onClick: () => setAsked(null),
37
+ "aria-label": "Close ask payload",
38
+ children: /* @__PURE__ */ jsx(Icon, { name: "fa-solid fa-xmark", className: "h-4 w-4" })
39
+ }
40
+ )
41
+ ] }),
42
+ /* @__PURE__ */ jsxs("div", { className: "bg-card min-h-0 flex-1 overflow-auto p-5", children: [
43
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground mb-3 text-sm", children: asked.suggestedPrompt }),
44
+ /* @__PURE__ */ jsx("pre", { className: "bg-muted overflow-x-auto rounded-md p-3 text-xs", children: JSON.stringify(asked.snapshot, null, 2) ?? "null" })
45
+ ] })
46
+ ] });
47
+ }
48
+ export {
49
+ AskAssistantBridge
50
+ };
@@ -0,0 +1,45 @@
1
+ import { WidgetConfig } from '@ekanos/sdk';
2
+ import { type WidgetRenderState } from '@ekanos/sdk/components';
3
+ /**
4
+ * A copy of the host's workspace dashboard grid.
5
+ *
6
+ * WHAT CHANGED, and why it had to be a copy rather than an import:
7
+ *
8
+ * - REMOVED the three server-action imports (`pinWidget`, `unpinWidget`,
9
+ * `setWidgetCollapsed`). They are `'use server'` + `server-only` + Supabase,
10
+ * which is the entire reason the real file cannot be imported here.
11
+ *
12
+ * - SWAPPED the host's `DashboardWidgetProvider` for `HarnessWidgetProvider`,
13
+ * which fills the SAME published `WidgetContext` from local state. The host
14
+ * provider is host chrome and is not on the `@ekanos/sdk` surface, because
15
+ * what makes it host chrome is the widget-config persistence this app has
16
+ * no database for. See the long note in `harness-widget-provider.tsx`; the
17
+ * resolved render state is identical.
18
+ *
19
+ * - ADDED `stateOverride`, so the dev toolbar can drive the REAL provider
20
+ * state machine. This is not cosmetic: `WidgetConfig.widgetState` is typed
21
+ * `'active' | 'inactive' | 'disabled'` and cannot express `loading` or
22
+ * `error`, even though the provider accepts all five. Without
23
+ * this the toolbar could not reach two of the five states at all.
24
+ *
25
+ * - ADDED a `WidgetBoundary` around each widget so one partner's crash
26
+ * renders a readable card instead of blanking the page.
27
+ *
28
+ * Everything else — grid classes, row grouping, the props handed to the
29
+ * provider, the two props handed to the widget — is verbatim. If the real grid
30
+ * changes, this needs the same change.
31
+ */
32
+ type DashboardGridLayout = 'masonry' | 'two-column';
33
+ interface DashboardGridProps {
34
+ widgets: WidgetConfig[];
35
+ accountId: string;
36
+ layout?: DashboardGridLayout;
37
+ /** Forced render state from the dev toolbar. */
38
+ stateOverride?: WidgetRenderState;
39
+ /** Forced collapsed state from the dev toolbar. */
40
+ collapsedOverride?: boolean;
41
+ /** Remount key — changing it resets crashed widget boundaries. */
42
+ resetKey?: string;
43
+ }
44
+ export declare function DashboardGrid({ widgets, accountId, layout, stateOverride, collapsedOverride, resetKey, }: DashboardGridProps): import("react").JSX.Element;
45
+ export {};
@@ -0,0 +1,84 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { HarnessWidgetProvider } from "./harness-widget-provider.js";
4
+ import { buildRowGroups, splitBalanced } from "./row-groups.js";
5
+ import { WidgetBoundary } from "./widget-boundary.js";
6
+ function DashboardGrid({
7
+ widgets,
8
+ accountId,
9
+ layout = "masonry",
10
+ stateOverride,
11
+ collapsedOverride,
12
+ resetKey
13
+ }) {
14
+ const shared = { accountId, stateOverride, collapsedOverride, resetKey };
15
+ if (layout === "two-column") {
16
+ return /* @__PURE__ */ jsx("div", { className: "@container/dashboard grid grid-cols-1 items-start gap-5 @4xl/dashboard:grid-cols-2", children: widgets.map((widget) => /* @__PURE__ */ jsx(WidgetRenderer, { widget, ...shared }, widget.id)) });
17
+ }
18
+ const rowGroups = buildRowGroups(widgets);
19
+ return /* @__PURE__ */ jsx("div", { className: "@container/dashboard space-y-5", children: rowGroups.map((group) => {
20
+ if (group.type === "full") {
21
+ return /* @__PURE__ */ jsx(
22
+ WidgetRenderer,
23
+ {
24
+ widget: group.widget,
25
+ ...shared
26
+ },
27
+ group.widget.id
28
+ );
29
+ }
30
+ const { left, right } = splitBalanced(group.widgets);
31
+ return /* @__PURE__ */ jsxs(
32
+ "div",
33
+ {
34
+ className: "grid grid-cols-1 gap-5 @4xl/dashboard:grid-cols-2 @4xl/dashboard:items-start",
35
+ children: [
36
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-5", children: left.map((widget) => /* @__PURE__ */ jsx(WidgetRenderer, { widget, ...shared }, widget.id)) }),
37
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-5", children: right.map((widget) => /* @__PURE__ */ jsx(WidgetRenderer, { widget, ...shared }, widget.id)) })
38
+ ]
39
+ },
40
+ group.widgets[0].id
41
+ );
42
+ }) });
43
+ }
44
+ function WidgetRenderer({
45
+ widget,
46
+ accountId,
47
+ stateOverride,
48
+ collapsedOverride,
49
+ resetKey
50
+ }) {
51
+ const Component = widget.component;
52
+ const state = stateOverride ?? widget.widgetState;
53
+ return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
54
+ HarnessWidgetProvider,
55
+ {
56
+ widgetId: widget.id,
57
+ title: widget.name,
58
+ state,
59
+ isEnabled: state === "active",
60
+ isCollapsible: widget.isCollapsible ?? false,
61
+ isPinnable: widget.isPinnable ?? false,
62
+ defaultCollapsed: collapsedOverride ?? widget.collapsed ?? false,
63
+ defaultPinned: widget.isPinned ?? false,
64
+ maxContentHeight: getMaxContentHeight(widget),
65
+ aiFooterEnabled: widget.aiFooterEnabled,
66
+ health: widget.health,
67
+ children: /* @__PURE__ */ jsx(WidgetBoundary, { widgetId: widget.id, resetKey: `${state}-${resetKey}`, children: /* @__PURE__ */ jsx(
68
+ Component,
69
+ {
70
+ accountId,
71
+ productId: widget.integrationMetadata?.productId ?? "fusion"
72
+ }
73
+ ) })
74
+ },
75
+ `${state}-${String(collapsedOverride)}-${resetKey ?? ""}`
76
+ ) });
77
+ }
78
+ function getMaxContentHeight(widget) {
79
+ const maxHeight = widget.layouts?.lg?.maxHeight;
80
+ return maxHeight ? `${maxHeight}px` : void 0;
81
+ }
82
+ export {
83
+ DashboardGrid
84
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The persistent dev toolbar. Every control here drives the real machinery
3
+ * rather than a harness-local imitation:
4
+ *
5
+ * - Render state feeds `DashboardWidgetProvider`'s `state` prop, so you are
6
+ * exercising the actual state machine, including the `isEnabled` and health
7
+ * downgrades it computes internally.
8
+ * - Fixture variant rebuilds the QueryClient with a different seed set.
9
+ * - Collapsed feeds `defaultCollapsed`.
10
+ * - Unhealthy feeds `health`, reproducing the one production condition that
11
+ * silently downgrades an active widget to `disabled`.
12
+ *
13
+ * All choices persist to localStorage, so a reload keeps your place.
14
+ */
15
+ export declare function DevToolbar(): import("react").JSX.Element;
@@ -0,0 +1,155 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { usePathname } from "next/navigation";
4
+ import { Button } from "@ekanos/ui/button";
5
+ import { Icon } from "@ekanos/ui/icon";
6
+ import { Label } from "@ekanos/ui/label";
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue
13
+ } from "@ekanos/ui/select";
14
+ import { Switch } from "@ekanos/ui/switch";
15
+ import { cn } from "@ekanos/ui/utils";
16
+ import { useTheme } from "next-themes";
17
+ import {
18
+ DATA_MODES,
19
+ FIXTURE_VARIANTS,
20
+ findIntegration,
21
+ resolveEgress,
22
+ supportsLiveMode
23
+ } from "../../registry.js";
24
+ import {
25
+ RENDER_STATES,
26
+ VIEWPORTS,
27
+ useToolbar
28
+ } from "../lib/toolbar-context.js";
29
+ import { useHarnessRegistry } from "../registry-context.js";
30
+ function Field({
31
+ label,
32
+ children
33
+ }) {
34
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
35
+ /* @__PURE__ */ jsx(Label, { className: "text-muted-foreground text-xs", children: label }),
36
+ children
37
+ ] });
38
+ }
39
+ const DATA_MODE_LABELS = {
40
+ fixtures: "Fixtures",
41
+ live: "Live API"
42
+ };
43
+ function useCurrentIntegration() {
44
+ const integrations = useHarnessRegistry();
45
+ const pathname = usePathname();
46
+ const slug = pathname.split("/").filter(Boolean)[0] ?? null;
47
+ return slug ? findIntegration(integrations, slug) : null;
48
+ }
49
+ function DevToolbar() {
50
+ const { state, setState, reset } = useToolbar();
51
+ const { resolvedTheme, setTheme } = useTheme();
52
+ const integration = useCurrentIntegration();
53
+ const liveAvailable = supportsLiveMode(integration);
54
+ const live = liveAvailable && state.dataMode === "live";
55
+ return /* @__PURE__ */ jsx("div", { className: "bg-card sticky top-0 z-40 border-b", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-end gap-5 px-6 py-3", children: [
56
+ /* @__PURE__ */ jsx(Field, { label: "Render state", children: /* @__PURE__ */ jsxs(
57
+ Select,
58
+ {
59
+ value: state.renderState,
60
+ onValueChange: (value) => setState({ renderState: value }),
61
+ children: [
62
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "w-36", "data-test": "toolbar-render-state", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
63
+ /* @__PURE__ */ jsx(SelectContent, { children: RENDER_STATES.map((value) => /* @__PURE__ */ jsx(SelectItem, { value, children: value }, value)) })
64
+ ]
65
+ }
66
+ ) }),
67
+ /* @__PURE__ */ jsx(Field, { label: "Data", children: /* @__PURE__ */ jsxs(
68
+ Select,
69
+ {
70
+ value: liveAvailable ? state.dataMode : "fixtures",
71
+ onValueChange: (value) => setState({ dataMode: value }),
72
+ disabled: !liveAvailable,
73
+ children: [
74
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "w-32", "data-test": "toolbar-data-mode", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
75
+ /* @__PURE__ */ jsx(SelectContent, { children: DATA_MODES.map((value) => /* @__PURE__ */ jsx(SelectItem, { value, children: DATA_MODE_LABELS[value] }, value)) })
76
+ ]
77
+ }
78
+ ) }),
79
+ /* @__PURE__ */ jsx(Field, { label: "Fixtures", children: /* @__PURE__ */ jsxs(
80
+ Select,
81
+ {
82
+ value: state.variant,
83
+ onValueChange: (value) => setState({ variant: value }),
84
+ disabled: live,
85
+ children: [
86
+ /* @__PURE__ */ jsx(SelectTrigger, { className: "w-32", "data-test": "toolbar-variant", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
87
+ /* @__PURE__ */ jsx(SelectContent, { children: FIXTURE_VARIANTS.map((value) => /* @__PURE__ */ jsx(SelectItem, { value, children: value }, value)) })
88
+ ]
89
+ }
90
+ ) }),
91
+ /* @__PURE__ */ jsx(Field, { label: "Viewport", children: /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: Object.keys(VIEWPORTS).map((key) => /* @__PURE__ */ jsx(
92
+ Button,
93
+ {
94
+ size: "sm",
95
+ variant: state.viewport === key ? "default" : "outline",
96
+ onClick: () => setState({ viewport: key }),
97
+ children: VIEWPORTS[key].label
98
+ },
99
+ key
100
+ )) }) }),
101
+ /* @__PURE__ */ jsx(Field, { label: "Collapsed", children: /* @__PURE__ */ jsx(
102
+ Switch,
103
+ {
104
+ checked: state.collapsed,
105
+ onCheckedChange: (checked) => setState({ collapsed: checked }),
106
+ "aria-label": "Toggle collapsed"
107
+ }
108
+ ) }),
109
+ /* @__PURE__ */ jsx(Field, { label: "Unhealthy", children: /* @__PURE__ */ jsx(
110
+ Switch,
111
+ {
112
+ checked: state.unhealthy,
113
+ onCheckedChange: (checked) => setState({ unhealthy: checked }),
114
+ "aria-label": "Simulate unhealthy integration"
115
+ }
116
+ ) }),
117
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
118
+ live ? /* @__PURE__ */ jsxs(
119
+ "span",
120
+ {
121
+ className: "text-primary flex items-center gap-2 text-sm font-medium",
122
+ "data-test": "toolbar-live-badge",
123
+ children: [
124
+ /* @__PURE__ */ jsx(Icon, { name: "fa-solid fa-tower-broadcast", className: "h-4 w-4" }),
125
+ "Reaching ",
126
+ resolveEgress(integration).join(", ")
127
+ ]
128
+ }
129
+ ) : null,
130
+ /* @__PURE__ */ jsxs(
131
+ Button,
132
+ {
133
+ size: "sm",
134
+ variant: "outline",
135
+ onClick: () => setTheme(resolvedTheme === "dark" ? "light" : "dark"),
136
+ "aria-label": "Toggle theme",
137
+ children: [
138
+ /* @__PURE__ */ jsxs("span", { className: cn("flex items-center dark:hidden"), children: [
139
+ /* @__PURE__ */ jsx(Icon, { name: "fa-solid fa-moon", className: "mr-2 h-4 w-4" }),
140
+ "Dark"
141
+ ] }),
142
+ /* @__PURE__ */ jsxs("span", { className: cn("hidden items-center dark:flex"), children: [
143
+ /* @__PURE__ */ jsx(Icon, { name: "fa-solid fa-sun-bright", className: "mr-2 h-4 w-4" }),
144
+ "Light"
145
+ ] })
146
+ ]
147
+ }
148
+ ),
149
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: reset, children: "Reset" })
150
+ ] })
151
+ ] }) });
152
+ }
153
+ export {
154
+ DevToolbar
155
+ };
@@ -0,0 +1,16 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { DataMode, FixtureSeed, HarnessLiveMode } from '../../registry.js';
3
+ import type { CompiledFixture } from '../lib/http-fixtures.js';
4
+ export declare function HarnessProviders({ seeds, mode, live, egress, httpFixtures, slug, children, }: {
5
+ seeds: FixtureSeed[];
6
+ /** `fixtures` (default) or `live`. See `lib/harness-query-client.ts`. */
7
+ mode?: DataMode;
8
+ /** The integration's live-mode descriptor, when it declares one. */
9
+ live?: HarnessLiveMode;
10
+ /** Origins live mode may reach — resolved centrally, see `resolveEgress`. */
11
+ egress: readonly string[];
12
+ /** Compiled HTTP fixtures for the active variant. */
13
+ httpFixtures: readonly CompiledFixture[];
14
+ slug: string;
15
+ children: ReactNode;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,83 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { Suspense, useMemo } from "react";
4
+ import {
5
+ IntegrationActivationProvider
6
+ } from "@ekanos/sdk/hooks";
7
+ import { AskAssistantProvider } from "@ekanos/sdk/hooks";
8
+ import { WidgetEventProvider } from "@ekanos/sdk/hooks";
9
+ import { QueryClientProvider } from "@tanstack/react-query";
10
+ import { TeamAccountWorkspaceContextProvider } from "../../mocks/team-account-workspace.js";
11
+ import {
12
+ installHarnessFetch,
13
+ publishHarnessRouting
14
+ } from "../lib/harness-fetch-interceptor.js";
15
+ import {
16
+ createAllowlistedFetch,
17
+ createFixturesFetch,
18
+ logHarnessFetch
19
+ } from "../lib/harness-live-fetch.js";
20
+ import { createHarnessQueryClient } from "../lib/harness-query-client.js";
21
+ import { redactSensitive } from "../lib/redact.js";
22
+ import { AskAssistantBridge } from "./ask-assistant-bridge.js";
23
+ installHarnessFetch();
24
+ const fixtureActivationActions = {
25
+ activate: async (input) => {
26
+ console.info(
27
+ "[harness] activate (fixture, not persisted):",
28
+ redactSensitive(input)
29
+ );
30
+ return { success: true };
31
+ },
32
+ deactivate: async (input) => {
33
+ console.info(
34
+ "[harness] deactivate (fixture, not persisted):",
35
+ redactSensitive(input)
36
+ );
37
+ return { success: true };
38
+ }
39
+ };
40
+ function HarnessProviders({
41
+ seeds,
42
+ mode = "fixtures",
43
+ live,
44
+ egress,
45
+ httpFixtures,
46
+ slug,
47
+ children
48
+ }) {
49
+ const queryClient = useMemo(
50
+ () => createHarnessQueryClient(seeds, mode),
51
+ [seeds, mode]
52
+ );
53
+ const harnessFetch = useMemo(() => {
54
+ if (mode === "live") {
55
+ return createAllowlistedFetch(egress, { onCall: logHarnessFetch(slug) });
56
+ }
57
+ return createFixturesFetch();
58
+ }, [mode, egress, slug]);
59
+ useMemo(() => {
60
+ installHarnessFetch();
61
+ publishHarnessRouting({
62
+ mode,
63
+ fixtures: httpFixtures,
64
+ egress: mode === "live" ? egress : [],
65
+ onCall: ({ url, method, outcome }) => {
66
+ if (outcome === "refused") {
67
+ console.error(`[harness:${slug}] refused ${method} ${url}`);
68
+ return;
69
+ }
70
+ console.info(`[harness:${slug}] ${outcome} ${method} ${url}`);
71
+ }
72
+ });
73
+ }, [mode, httpFixtures, egress, slug]);
74
+ const FetchProvider = live?.FetchProvider;
75
+ const tree = /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(TeamAccountWorkspaceContextProvider, { children: /* @__PURE__ */ jsx(IntegrationActivationProvider, { actions: fixtureActivationActions, children: /* @__PURE__ */ jsx(WidgetEventProvider, { children: /* @__PURE__ */ jsxs(AskAssistantProvider, { children: [
76
+ /* @__PURE__ */ jsx(AskAssistantBridge, {}),
77
+ /* @__PURE__ */ jsx(Suspense, { fallback: null, children })
78
+ ] }) }) }) }) });
79
+ return FetchProvider ? /* @__PURE__ */ jsx(FetchProvider, { fetch: harnessFetch, children: tree }) : tree;
80
+ }
81
+ export {
82
+ HarnessProviders
83
+ };
@@ -0,0 +1,60 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { WidgetAskContext } from '@ekanos/sdk';
3
+ import { type WidgetHealth, type WidgetRenderState } from '@ekanos/sdk/components';
4
+ /**
5
+ * ─────────────────────────────────────────────────────────────────────────────
6
+ * The harness's widget context provider.
7
+ * ─────────────────────────────────────────────────────────────────────────────
8
+ *
9
+ * WHY THIS EXISTS — the one symbol the harness could not import.
10
+ *
11
+ * `Widget.*` reads everything (render state, collapse/pin, title, ask config)
12
+ * from `WidgetContext`, and something has to PROVIDE it. In the real app that is
13
+ * `DashboardWidgetProvider`, which lives in the host's own package and is not
14
+ * part of the published `@ekanos/sdk` surface — on purpose. It is coupled to
15
+ * things that only exist inside Fusion: `'use server'` persistence callbacks
16
+ * for the widget-config hierarchy, and the Supabase rows behind them. The SDK
17
+ * inversion drew the line exactly there: the SDK exports the CONSUMER side
18
+ * (`WidgetContext` plus `WidgetContextValue` / `WidgetState` / `WidgetActions` /
19
+ * `WidgetMeta` / `WidgetRenderState` / `WidgetHealth`) so any shell can fill it,
20
+ * and keeps the persisting provider host-side.
21
+ *
22
+ * So this is not a copy of an unpublished component — it is the documented
23
+ * alternative. `dashboard-widget-provider.tsx` says so in its own doc comment:
24
+ *
25
+ * "For non-persistent contexts (e.g. admin previews), use a different
26
+ * provider that implements the same `WidgetContextValue` interface but
27
+ * stores state locally only."
28
+ *
29
+ * That is precisely the harness: no database, so nothing to persist. Collapse
30
+ * and pin toggle locally and reset on reload, which is correct harness
31
+ * behaviour and is what the host provider did here anyway once its optional
32
+ * callbacks were omitted.
33
+ *
34
+ * Kept deliberately faithful in one non-obvious respect: the `isEnabled` and
35
+ * `health` downgrade rules below are the host's, verbatim, because a widget's
36
+ * `disabled` state is reached through them and a harness that resolved state
37
+ * differently would show a state the product never shows. `pendingCollapse` /
38
+ * `pendingPin` are always false — with no persistence callback there is never
39
+ * anything in flight, exactly as in the real grid when it passes none.
40
+ */
41
+ export interface HarnessWidgetProviderProps {
42
+ widgetId: string;
43
+ title?: string;
44
+ subtitle?: string;
45
+ description?: string;
46
+ className?: string;
47
+ state: WidgetRenderState;
48
+ isEnabled?: boolean;
49
+ isCollapsible?: boolean;
50
+ isPinnable?: boolean;
51
+ defaultCollapsed?: boolean;
52
+ defaultPinned?: boolean;
53
+ maxContentHeight?: string;
54
+ askContext?: WidgetAskContext;
55
+ aiFooterEnabled?: boolean;
56
+ /** Integration health; `unhealthy` downgrades the widget to `disabled`. */
57
+ health?: WidgetHealth;
58
+ children: ReactNode;
59
+ }
60
+ export declare function HarnessWidgetProvider({ widgetId, title, subtitle, description, className, state: renderState, isEnabled, isCollapsible, isPinnable, defaultCollapsed, defaultPinned, maxContentHeight, askContext, aiFooterEnabled, health, children, }: HarnessWidgetProviderProps): import("react").JSX.Element;