@matterfact/embed 0.7.0 → 0.9.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 (50) hide show
  1. package/README.md +171 -1
  2. package/dist/chunk-6EM7T2JV.js +816 -0
  3. package/dist/chunk-6EM7T2JV.js.map +1 -0
  4. package/dist/{chunk-4Q2ROXLR.js → chunk-BKKXHSYU.js} +2 -2
  5. package/dist/chunk-FEXG4LQJ.js +3 -0
  6. package/dist/chunk-FEXG4LQJ.js.map +7 -0
  7. package/dist/chunk-NWNMS34P.js +2 -0
  8. package/dist/chunk-URGQBG4I.js +800 -0
  9. package/dist/chunk-URGQBG4I.js.map +1 -0
  10. package/dist/context-ACFBWIFH.js +3 -0
  11. package/dist/{context-YX2KXFLI.js.map → context-ACFBWIFH.js.map} +1 -1
  12. package/dist/{context-WU2F5CBC.js → context-U2HJJN2S.js} +4 -2
  13. package/dist/embed.js +1 -1
  14. package/dist/embed.js.map +4 -4
  15. package/dist/index.cjs +1006 -159
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +192 -32
  18. package/dist/index.d.ts +192 -32
  19. package/dist/index.js +537 -130
  20. package/dist/index.js.map +1 -1
  21. package/dist/react.cjs +1042 -165
  22. package/dist/react.cjs.map +1 -1
  23. package/dist/react.d.cts +81 -1
  24. package/dist/react.d.ts +81 -1
  25. package/dist/react.js +566 -132
  26. package/dist/react.js.map +1 -1
  27. package/dist/{snapshot-JOGZWESK.js → snapshot-4GXT6PKZ.js} +2 -2
  28. package/dist/{snapshot-MUXE7KXX.js → snapshot-Y75SCGCM.js} +3 -3
  29. package/dist/{snapshot-MUXE7KXX.js.map → snapshot-Y75SCGCM.js.map} +1 -1
  30. package/examples/embed-demo/README.md +37 -0
  31. package/examples/embed-demo/src/App.tsx +55 -21
  32. package/examples/embed-demo/src/HoistDemo.tsx +390 -0
  33. package/examples/embed-demo/src/main.tsx +7 -0
  34. package/examples/embed-demo/src/mockXH.ts +492 -0
  35. package/examples/embed-demo/src/placement.tsx +43 -0
  36. package/examples/embed-demo/src/styles.css +147 -2
  37. package/examples/embed-demo/vite.config.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/chunk-7I37ZFAJ.js +0 -2
  40. package/dist/chunk-AXKXNYRT.js +0 -381
  41. package/dist/chunk-AXKXNYRT.js.map +0 -1
  42. package/dist/chunk-D7R6WVHG.js +0 -2
  43. package/dist/chunk-D7R6WVHG.js.map +0 -7
  44. package/dist/chunk-RS6RMZ77.js +0 -396
  45. package/dist/chunk-RS6RMZ77.js.map +0 -1
  46. package/dist/context-YX2KXFLI.js +0 -3
  47. /package/dist/{chunk-4Q2ROXLR.js.map → chunk-BKKXHSYU.js.map} +0 -0
  48. /package/dist/{chunk-7I37ZFAJ.js.map → chunk-NWNMS34P.js.map} +0 -0
  49. /package/dist/{context-WU2F5CBC.js.map → context-U2HJJN2S.js.map} +0 -0
  50. /package/dist/{snapshot-JOGZWESK.js.map → snapshot-4GXT6PKZ.js.map} +0 -0
@@ -0,0 +1,390 @@
1
+ import { useState } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import { MatterfactAgent } from '@matterfact/embed/react';
4
+ import { config } from './config';
5
+ import { PlacementSwitch, usePlacement } from './placement';
6
+ import {
7
+ useMockXH,
8
+ mockNavigate,
9
+ selectHolding,
10
+ getHoldings,
11
+ getOwnershipRows,
12
+ getPriceSeries,
13
+ } from './mockXH';
14
+
15
+ type NavPolicy = 'off' | 'confirm' | 'auto';
16
+
17
+ /**
18
+ * The `?hoist=1` demo: the SAME Meridian Capital portfolio App.tsx renders, but
19
+ * with NOTHING declared to the agent by hand — `window.XH` (installed by
20
+ * mockXH.ts, before this ever mounts) is all `<MatterfactAgent>` needs. No
21
+ * `getPageContext` prop here on purpose: leaving it unset is what lets the SDK's
22
+ * automatic Hoist detection run (see @matterfact/embed's context.ts —
23
+ * `resolveDeclaredContext` only reaches the Hoist fallback when no declared
24
+ * context source is present).
25
+ */
26
+ export function HoistDemo() {
27
+ const { name, params, selection } = useMockXH();
28
+ const [panelOpen, setPanelOpen] = useState(true);
29
+ const [placement, setPlacement] = usePlacement();
30
+ const [navPolicy, setNavPolicy] = useState<NavPolicy>('confirm');
31
+ const ticker = params.ticker;
32
+ const onCompanyPage = name.startsWith('default.company.ticker');
33
+
34
+ return (
35
+ <div
36
+ className="app"
37
+ data-panel={panelOpen ? 'open' : 'closed'}
38
+ data-placement={placement}
39
+ >
40
+ <header>
41
+ <div className="brand">
42
+ <span className="mark">M</span> Meridian Capital{' '}
43
+ <small>· Hoist mock</small>
44
+ </div>
45
+ <nav>
46
+ <NavButton
47
+ active={name === 'default.holdings'}
48
+ onClick={() => mockNavigate('default.holdings')}
49
+ >
50
+ Holdings
51
+ </NavButton>
52
+ <NavButton
53
+ active={name === 'default.research'}
54
+ onClick={() => mockNavigate('default.research')}
55
+ >
56
+ Research
57
+ </NavButton>
58
+ {onCompanyPage && (
59
+ <>
60
+ <span className="nav-sep">/</span>
61
+ <NavButton active onClick={() => {}}>
62
+ {ticker}
63
+ </NavButton>
64
+ </>
65
+ )}
66
+ </nav>
67
+ <span className="spacer" />
68
+ <a className="mode-link" href={location.pathname}>
69
+ ← Hand-declared mode
70
+ </a>
71
+ <span className="env-chip">
72
+ {config.env} · {config.widgetOrigin.replace(/^https?:\/\//, '')}
73
+ </span>
74
+ <label className="hoist-policy">
75
+ hoist.navigate
76
+ <select
77
+ value={navPolicy}
78
+ onChange={(e) => setNavPolicy(e.target.value as NavPolicy)}
79
+ >
80
+ <option value="off">Off</option>
81
+ <option value="confirm">Confirm</option>
82
+ <option value="auto">Auto</option>
83
+ </select>
84
+ </label>
85
+ <PlacementSwitch placement={placement} onChange={setPlacement} />
86
+ {placement === 'panel' && (
87
+ <button className="toggle" onClick={() => setPanelOpen((v) => !v)}>
88
+ <span className="dot" /> Assistant
89
+ </button>
90
+ )}
91
+ </header>
92
+
93
+ {/* THE CORNER — the widget's own box: it positions and sizes itself, so it
94
+ sits outside the layout with nothing wrapped around it. */}
95
+ {placement === 'corner' && (
96
+ <MatterfactAgent
97
+ publishableKey={config.publishableKey}
98
+ widgetOrigin={config.widgetOrigin}
99
+ surface={config.surface}
100
+ theme={config.theme}
101
+ actions={{ navigate: navPolicy }}
102
+ />
103
+ )}
104
+
105
+ <div className="layout">
106
+ <main>
107
+ <HoistPage
108
+ routeName={name}
109
+ ticker={ticker}
110
+ selection={selection}
111
+ onCompanyPage={onCompanyPage}
112
+ />
113
+ </main>
114
+
115
+ {/* THE PANEL — same inline-agent pattern as App.tsx's hand-declared demo. */}
116
+ {placement === 'panel' && (
117
+ <aside>
118
+ <div className="panel-head">
119
+ <span className="mf">✦</span> matterfact assistant
120
+ </div>
121
+ <div className="agent-slot">
122
+ <MatterfactAgent
123
+ inline
124
+ publishableKey={config.publishableKey}
125
+ widgetOrigin={config.widgetOrigin}
126
+ surface={config.surface}
127
+ theme={config.theme}
128
+ actions={{ navigate: navPolicy }}
129
+ style={{ width: '100%', height: '100%' }}
130
+ />
131
+ </div>
132
+ </aside>
133
+ )}
134
+ </div>
135
+ </div>
136
+ );
137
+ }
138
+
139
+ function NavButton({
140
+ active,
141
+ onClick,
142
+ children,
143
+ }: {
144
+ active?: boolean;
145
+ onClick: () => void;
146
+ children: ReactNode;
147
+ }) {
148
+ return (
149
+ <button className={active ? 'active' : undefined} onClick={onClick}>
150
+ {children}
151
+ </button>
152
+ );
153
+ }
154
+
155
+ function HoistPage({
156
+ routeName,
157
+ ticker,
158
+ selection,
159
+ onCompanyPage,
160
+ }: {
161
+ routeName: string;
162
+ ticker: string;
163
+ selection: string | null;
164
+ onCompanyPage: boolean;
165
+ }) {
166
+ if (onCompanyPage) {
167
+ const leaf = routeName.split('.').pop() ?? 'thesis';
168
+ return <CompanyPage ticker={ticker} leaf={leaf} />;
169
+ }
170
+ if (routeName === 'default.research') return <ResearchPage />;
171
+ return <HoldingsPage selection={selection} />;
172
+ }
173
+
174
+ function HoldingsPage({ selection }: { selection: string | null }) {
175
+ const holdings = getHoldings();
176
+ const selected = holdings.find((h) => h.ticker === selection);
177
+ return (
178
+ <>
179
+ <div className="eyebrow">Portfolio</div>
180
+ <h1>Holdings</h1>
181
+ <div className="sub">
182
+ {holdings.length} holdings · read live from a mock{' '}
183
+ <code>window.XH</code> GridModel
184
+ </div>
185
+
186
+ <div className="section-title">
187
+ Holdings{' '}
188
+ <span className="hint">
189
+ (click a row to select it · click a ticker to drill in)
190
+ </span>
191
+ </div>
192
+ <table>
193
+ <thead>
194
+ <tr>
195
+ <th>Ticker</th>
196
+ <th>Weight</th>
197
+ <th>Day</th>
198
+ <th>YTD</th>
199
+ </tr>
200
+ </thead>
201
+ <tbody>
202
+ {holdings.map((h) => (
203
+ <tr
204
+ key={h.ticker}
205
+ aria-pressed={h.ticker === selection}
206
+ onClick={() => selectHolding(h.ticker)}
207
+ style={{
208
+ cursor: 'pointer',
209
+ background:
210
+ h.ticker === selection ? 'var(--surface-2)' : undefined,
211
+ }}
212
+ >
213
+ <td className="tick">
214
+ {h.ticker === selection ? '▸ ' : ''}
215
+ <button
216
+ className="ticker-link"
217
+ onClick={(e) => {
218
+ e.stopPropagation();
219
+ mockNavigate('default.company.ticker.thesis', {
220
+ ticker: h.ticker,
221
+ });
222
+ }}
223
+ >
224
+ {h.ticker}
225
+ </button>
226
+ </td>
227
+ <td>{h.weight}</td>
228
+ <td className={h.dayNeg ? 'neg' : 'pos'}>{h.day}</td>
229
+ <td className={h.ytdNeg ? 'neg' : 'pos'}>{h.ytd}</td>
230
+ </tr>
231
+ ))}
232
+ </tbody>
233
+ </table>
234
+ <p className="artifact-caption">
235
+ <strong>Automatic context:</strong> this table, the selection, and the
236
+ site map in the header are all read live out of <code>window.XH</code> —
237
+ nothing on this page is declared to the agent by hand. Select a row,
238
+ then ask <em>"what's selected?"</em>; ask the assistant to navigate to
239
+ Research or to a holding's Ownership tab and watch the approval card.
240
+ </p>
241
+ {selected && (
242
+ <p className="artifact-caption">
243
+ <strong>Selected:</strong> {selected.ticker} — {selected.thesis}
244
+ </p>
245
+ )}
246
+ </>
247
+ );
248
+ }
249
+
250
+ function ResearchPage() {
251
+ return (
252
+ <>
253
+ <div className="eyebrow">Research</div>
254
+ <h1>Research</h1>
255
+ <div className="sub">
256
+ No grid or chart is mounted on this page — just a route and a site-map.
257
+ </div>
258
+ <p className="artifact-caption">
259
+ With no GridModel or ChartModel present, the agent's page context comes
260
+ from the route and nav alone (<code>buildHoistContext</code> in the
261
+ adapter) — a good page to ask <em>"what page am I on?"</em> against.
262
+ </p>
263
+ </>
264
+ );
265
+ }
266
+
267
+ function CompanyPage({ ticker, leaf }: { ticker: string; leaf: string }) {
268
+ const holding = getHoldings().find((h) => h.ticker === ticker);
269
+ return (
270
+ <>
271
+ <div className="eyebrow">Company · {ticker}</div>
272
+ <h1>{ticker}</h1>
273
+ <div className="company-subnav">
274
+ <SubTab
275
+ active={leaf === 'thesis'}
276
+ onClick={() =>
277
+ mockNavigate('default.company.ticker.thesis', { ticker })
278
+ }
279
+ >
280
+ Thesis
281
+ </SubTab>
282
+ <SubTab
283
+ active={leaf === 'financials'}
284
+ onClick={() =>
285
+ mockNavigate('default.company.ticker.financials', { ticker })
286
+ }
287
+ >
288
+ Financials
289
+ </SubTab>
290
+ <SubTab
291
+ active={leaf === 'ownership'}
292
+ onClick={() =>
293
+ mockNavigate('default.company.ticker.ownership', { ticker })
294
+ }
295
+ >
296
+ Ownership
297
+ </SubTab>
298
+ </div>
299
+
300
+ {leaf === 'thesis' && (
301
+ <p className="artifact-caption" style={{ fontSize: 14 }}>
302
+ {holding?.thesis ?? 'No thesis on file for this ticker.'}
303
+ </p>
304
+ )}
305
+ {leaf === 'financials' && (
306
+ <>
307
+ <div className="section-title">Indexed price</div>
308
+ <Sparkline points={getPriceSeries(ticker)} />
309
+ <p className="artifact-caption">
310
+ Backed by a mock <code>ChartModel</code> with{' '}
311
+ <code>[timestamp, value]</code> series points — the agent sees a
312
+ points/min/max/last summary, never the raw series.
313
+ </p>
314
+ </>
315
+ )}
316
+ {leaf === 'ownership' && (
317
+ <>
318
+ <div className="section-title">Top holders</div>
319
+ <table>
320
+ <thead>
321
+ <tr>
322
+ <th>Holder</th>
323
+ <th>Shares</th>
324
+ <th>% Owned</th>
325
+ </tr>
326
+ </thead>
327
+ <tbody>
328
+ {getOwnershipRows(ticker).map((r) => (
329
+ <tr key={r.holder}>
330
+ <td>{r.holder}</td>
331
+ <td>{r.shares}</td>
332
+ <td>{r.pct}</td>
333
+ </tr>
334
+ ))}
335
+ </tbody>
336
+ </table>
337
+ </>
338
+ )}
339
+ </>
340
+ );
341
+ }
342
+
343
+ function SubTab({
344
+ active,
345
+ onClick,
346
+ children,
347
+ }: {
348
+ active: boolean;
349
+ onClick: () => void;
350
+ children: ReactNode;
351
+ }) {
352
+ return (
353
+ <button className={`subtab${active ? ' active' : ''}`} onClick={onClick}>
354
+ {children}
355
+ </button>
356
+ );
357
+ }
358
+
359
+ /** A dependency-free inline line chart — visual proof that the page the agent
360
+ * navigated to is a real, different view, not just a route-name change. */
361
+ function Sparkline({ points }: { points: number[] }) {
362
+ if (!points.length) return null;
363
+ const width = 480;
364
+ const height = 120;
365
+ const min = Math.min(...points);
366
+ const max = Math.max(...points);
367
+ const span = max - min || 1;
368
+ const stepX = points.length > 1 ? width / (points.length - 1) : 0;
369
+ const d = points
370
+ .map(
371
+ (v, i) =>
372
+ `${i === 0 ? 'M' : 'L'} ${(i * stepX).toFixed(1)} ${(
373
+ height -
374
+ ((v - min) / span) * height
375
+ ).toFixed(1)}`,
376
+ )
377
+ .join(' ');
378
+ return (
379
+ <svg
380
+ width={width}
381
+ height={height}
382
+ viewBox={`0 0 ${width} ${height}`}
383
+ className="hoist-chart"
384
+ role="img"
385
+ aria-label="Indexed price chart"
386
+ >
387
+ <path d={d} fill="none" stroke="var(--accent)" strokeWidth={2} />
388
+ </svg>
389
+ );
390
+ }
@@ -1,8 +1,15 @@
1
1
  import { StrictMode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
3
  import { App } from './App';
4
+ import { installMockXH, isHoistModeEnabled } from './mockXH';
4
5
  import './styles.css';
5
6
 
7
+ // Installed BEFORE React renders anything, not from an effect: the widget's mount
8
+ // effect reads window.XH synchronously (see @matterfact/embed's context.ts —
9
+ // start() calls advertiseTools() at the end of mount), so window.XH has to exist
10
+ // before <MatterfactAgent> ever mounts. See mockXH.ts's doc comment.
11
+ if (isHoistModeEnabled()) installMockXH();
12
+
6
13
  createRoot(document.getElementById('root')!).render(
7
14
  <StrictMode>
8
15
  <App />