@robosystems/core 0.8.7 → 0.8.9

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosystems/core",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "Shared RoboSystems frontend core library (auth, contexts, task monitoring, UI components) for the RoboSystems Next.js apps",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -11,7 +11,8 @@ export interface MarkdownProseProps {
11
11
  * search results). Theme-aware via `dark:prose-dark` — the consuming apps
12
12
  * define the `typography.dark` variant in their Tailwind configs; do NOT
13
13
  * switch to `dark:prose-invert`, which doesn't resolve under their
14
- * Tailwind v4 + typography-plugin setup (see research/ResearchArticle.tsx).
14
+ * Tailwind v4 + typography-plugin setup (first hit in the research article,
15
+ * now roboinvestor-app's src/components/research/ResearchArticle.tsx).
15
16
  * Works in server and client components alike.
16
17
  */
17
18
  export declare function MarkdownProse({ children, size, className, }: MarkdownProseProps): import("react").JSX.Element;
@@ -7,7 +7,8 @@ import { twMerge } from 'tailwind-merge';
7
7
  * search results). Theme-aware via `dark:prose-dark` — the consuming apps
8
8
  * define the `typography.dark` variant in their Tailwind configs; do NOT
9
9
  * switch to `dark:prose-invert`, which doesn't resolve under their
10
- * Tailwind v4 + typography-plugin setup (see research/ResearchArticle.tsx).
10
+ * Tailwind v4 + typography-plugin setup (first hit in the research article,
11
+ * now roboinvestor-app's src/components/research/ResearchArticle.tsx).
11
12
  * Works in server and client components alike.
12
13
  */
13
14
  export function MarkdownProse({ children, size = 'base', className, }) {
@@ -17,6 +17,12 @@ export interface LandingFooterProps {
17
17
  isOpen: boolean;
18
18
  onClose: () => void;
19
19
  }>;
20
+ /**
21
+ * Where the Company column's Blog link goes. Defaults to the platform blog on
22
+ * robosystems.ai; an app with its own blog lane (roboledger.ai/blog since
23
+ * 2026-09-02) passes its own path so the footer sends readers to that lane.
24
+ */
25
+ blogHref?: string;
20
26
  }
21
27
  /**
22
28
  * The shared marketing-site footer for all three apps. The brand column, social
@@ -25,4 +31,4 @@ export interface LandingFooterProps {
25
31
  * only the tagline and Product links vary and are passed in. Contact stays
26
32
  * per-app via the `contactModal` prop.
27
33
  */
28
- export declare function LandingFooter({ tagline, productLinks, contactModal: ContactModal, }: LandingFooterProps): import("react").JSX.Element;
34
+ export declare function LandingFooter({ tagline, productLinks, contactModal: ContactModal, blogHref, }: LandingFooterProps): import("react").JSX.Element;
@@ -15,20 +15,33 @@ const headingClass = 'mb-4 text-sm font-semibold tracking-wider text-gray-400 up
15
15
  * only the tagline and Product links vary and are passed in. Contact stays
16
16
  * per-app via the `contactModal` prop.
17
17
  */
18
- export function LandingFooter({ tagline, productLinks, contactModal: ContactModal, }) {
18
+ export function LandingFooter({ tagline, productLinks, contactModal: ContactModal, blogHref, }) {
19
19
  const [showContact, setShowContact] = useState(false);
20
20
  const current = getAppConfig(CURRENT_APP);
21
21
  const isRoboSystems = CURRENT_APP === 'robosystems';
22
- // RoboSystems serves Blog/Privacy/Terms itself; the other apps link out to it.
22
+ // RoboSystems serves the platform blog, Privacy and Terms itself; the other
23
+ // apps link out to it.
23
24
  const companyBase = isRoboSystems ? '' : getAppConfig('robosystems').url;
24
25
  const companyLinkProps = isRoboSystems
25
26
  ? {}
26
27
  : { target: '_blank', rel: 'noopener noreferrer' };
28
+ // Research lives on roboinvestor.ai (robosystems.ai redirects there since
29
+ // 2026-09-02), so it resolves against that app, not the platform site.
30
+ const isRoboInvestor = CURRENT_APP === 'roboinvestor';
31
+ const researchHref = isRoboInvestor
32
+ ? '/research'
33
+ : `${getAppConfig('roboinvestor').url}/research`;
34
+ const researchLinkProps = isRoboInvestor
35
+ ? {}
36
+ : { target: '_blank', rel: 'noopener noreferrer' };
37
+ const resolvedBlogHref = blogHref !== null && blogHref !== void 0 ? blogHref : `${companyBase}/blog`;
38
+ // A same-site path opens in the same tab; an external site opens in a new one.
39
+ const blogLinkProps = resolvedBlogHref.startsWith('/') ? {} : companyLinkProps;
27
40
  return (_jsxs("footer", { className: "border-t border-gray-800 bg-zinc-950", children: [_jsxs("div", { className: "mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8", children: [_jsxs("div", { className: "grid gap-4 md:grid-cols-5 lg:gap-6", children: [_jsxs("div", { className: "md:col-span-2", children: [_jsxs("div", { className: "mb-4 flex items-center gap-2", children: [_jsx(Image, { src: `/images/logos/${CURRENT_APP}.png`, alt: current.displayName, width: 32, height: 32, className: "rounded-lg" }), _jsx("span", { className: "font-heading text-xl font-semibold text-white", children: current.displayName })] }), _jsx("p", { className: "mb-4 max-w-md text-sm text-gray-400", children: tagline }), _jsxs("div", { className: "flex gap-4", children: [_jsx(Link, { href: "https://github.com/RoboFinSystems", target: "_blank", rel: "noopener noreferrer", "aria-label": "RoboSystems on GitHub", className: linkClass, children: _jsx("svg", { className: "h-6 w-6", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" }) }) }), _jsx(Link, { href: "https://x.com/robofinsystems", target: "_blank", rel: "noopener noreferrer", "aria-label": "RoboSystems on X", className: linkClass, children: _jsx("svg", { className: "h-6 w-6", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" }) }) }), _jsx(Link, { href: "https://www.linkedin.com/company/robosystems", target: "_blank", rel: "noopener noreferrer", "aria-label": "RoboSystems on LinkedIn", className: linkClass, children: _jsx("svg", { className: "h-6 w-6", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" }) }) }), _jsx(Link, { href: "https://www.youtube.com/@RoboSystems", target: "_blank", rel: "noopener noreferrer", "aria-label": "RoboSystems on YouTube", className: linkClass, children: _jsx("svg", { className: "h-6 w-6", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" }) }) })] })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Product" }), _jsx("ul", { className: "space-y-2 text-sm", children: productLinks.map((link) => (_jsx("li", { children: _jsx(Link, { href: link.href, className: linkClass, children: link.label }) }, link.href))) })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Applications" }), _jsx("ul", { className: "space-y-2 text-sm", children: APP_ORDER.map((app) => {
28
41
  const cfg = getAppConfig(app);
29
42
  const isCurrent = app === CURRENT_APP;
30
43
  return (_jsx("li", { children: _jsx(Link, Object.assign({ href: cfg.url }, (isCurrent
31
44
  ? {}
32
45
  : { target: '_blank', rel: 'noopener noreferrer' }), { className: linkClass, children: cfg.displayName })) }, app));
33
- }) })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Company" }), _jsxs("ul", { className: "space-y-2 text-sm", children: [_jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/research` }, companyLinkProps, { className: linkClass, children: "Research" })) }), _jsx("li", { children: _jsx(Link, { href: "https://holon.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Holon Viewer" }) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/blog` }, companyLinkProps, { className: linkClass, children: "Blog" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/privacy` }, companyLinkProps, { className: linkClass, children: "Privacy" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/terms` }, companyLinkProps, { className: linkClass, children: "Terms" })) }), _jsx("li", { children: _jsx(Link, { href: "https://trust.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Trust Center" }) }), _jsx("li", { children: _jsx(Link, { href: "https://status.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Status" }) }), ContactModal && (_jsx("li", { children: _jsx("button", { onClick: () => setShowContact(true), className: linkClass, children: "Contact" }) }))] })] })] }), _jsx("div", { className: "mt-8 border-t border-gray-800 pt-8 text-center text-sm text-gray-400", children: _jsxs("p", { children: ["\u00A9 ", new Date().getFullYear(), " RFS LLC. All rights reserved."] }) })] }), ContactModal && (_jsx(ContactModal, { isOpen: showContact, onClose: () => setShowContact(false) }))] }));
46
+ }) })] }), _jsxs("div", { children: [_jsx("h3", { className: headingClass, children: "Company" }), _jsxs("ul", { className: "space-y-2 text-sm", children: [_jsx("li", { children: _jsx(Link, Object.assign({ href: researchHref }, researchLinkProps, { className: linkClass, children: "Research" })) }), _jsx("li", { children: _jsx(Link, { href: "https://xbrlkit.com", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "xbrlkit viewer" }) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: resolvedBlogHref }, blogLinkProps, { className: linkClass, children: "Blog" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/privacy` }, companyLinkProps, { className: linkClass, children: "Privacy" })) }), _jsx("li", { children: _jsx(Link, Object.assign({ href: `${companyBase}/pages/terms` }, companyLinkProps, { className: linkClass, children: "Terms" })) }), _jsx("li", { children: _jsx(Link, { href: "https://trust.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Trust Center" }) }), _jsx("li", { children: _jsx(Link, { href: "https://status.robosystems.ai", target: "_blank", rel: "noopener noreferrer", className: linkClass, children: "Status" }) }), ContactModal && (_jsx("li", { children: _jsx("button", { onClick: () => setShowContact(true), className: linkClass, children: "Contact" }) }))] })] })] }), _jsx("div", { className: "mt-8 border-t border-gray-800 pt-8 text-center text-sm text-gray-400", children: _jsxs("p", { children: ["\u00A9 ", new Date().getFullYear(), " RFS LLC. All rights reserved."] }) })] }), ContactModal && (_jsx(ContactModal, { isOpen: showContact, onClose: () => setShowContact(false) }))] }));
34
47
  }
@@ -1,19 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- export interface CoverageBrowserProps {
3
- items: CoverageItem[];
4
- /** Base path for card links, forwarded to the grid (defaults to /research). */
5
- hrefBase?: string;
6
- /** Override the search box placeholder. */
7
- placeholder?: string;
8
- }
9
- /**
10
- * The research listing with a live client-side filter above it. A drop-in
11
- * replacement for {@link CoverageGrid}: the full grid renders on first paint
12
- * (so the server-rendered SEO page keeps every card in its HTML), and typing
13
- * narrows it in the browser. Matches ticker, company, title, and tags —
14
- * someone who types "Green Thumb" finds GTBIF without knowing the symbol.
15
- *
16
- * Filtering the whole in-memory catalog is fine at this scale; when the
17
- * catalog outgrows a single fetch this is where server-backed search slots in.
18
- */
19
- export declare function CoverageBrowser({ items, hrefBase, placeholder, }: CoverageBrowserProps): import("react").JSX.Element;
@@ -1,35 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { TextInput } from 'flowbite-react';
4
- import { useMemo, useState } from 'react';
5
- import { HiSearch } from 'react-icons/hi';
6
- import { CoverageGrid } from './CoverageGrid';
7
- /**
8
- * The research listing with a live client-side filter above it. A drop-in
9
- * replacement for {@link CoverageGrid}: the full grid renders on first paint
10
- * (so the server-rendered SEO page keeps every card in its HTML), and typing
11
- * narrows it in the browser. Matches ticker, company, title, and tags —
12
- * someone who types "Green Thumb" finds GTBIF without knowing the symbol.
13
- *
14
- * Filtering the whole in-memory catalog is fine at this scale; when the
15
- * catalog outgrows a single fetch this is where server-backed search slots in.
16
- */
17
- export function CoverageBrowser({ items, hrefBase, placeholder = 'Search by ticker, company, or title…', }) {
18
- const [query, setQuery] = useState('');
19
- const filtered = useMemo(() => {
20
- const term = query.trim().toLowerCase();
21
- if (!term)
22
- return items;
23
- return items.filter((item) => item.ticker.toLowerCase().includes(term) ||
24
- item.company.toLowerCase().includes(term) ||
25
- item.title.toLowerCase().includes(term) ||
26
- item.tags.some((tag) => tag.toLowerCase().includes(term)));
27
- }, [items, query]);
28
- // Empty catalog: let the grid render its own "no coverage yet" state, no
29
- // point showing a search box over nothing.
30
- if (!items.length) {
31
- return _jsx(CoverageGrid, { items: items, hrefBase: hrefBase });
32
- }
33
- const term = query.trim();
34
- return (_jsxs("div", { className: "space-y-6", children: [_jsx(TextInput, { icon: HiSearch, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: placeholder, "aria-label": "Search research" }), term && filtered.length === 0 ? (_jsxs("p", { className: "py-12 text-center text-gray-500 dark:text-gray-400", children: ["No results for \u201C", term, "\u201D."] })) : (_jsx(CoverageGrid, { items: filtered, hrefBase: hrefBase }))] }));
35
- }
@@ -1,12 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- /**
3
- * A coverage tile: full-bleed thumbnail on top, then ticker/label, title, summary, and
4
- * prior-report count. Self-contained (not the shared Flowbite Card) so the cover image
5
- * sits flush at the top of every card regardless of how much text each one has — the
6
- * shared card theme centers its content (`justify-center`), which left the covers at
7
- * different heights across a row.
8
- */
9
- export declare function CoverageCard({ item, hrefBase, }: {
10
- item: CoverageItem;
11
- hrefBase?: string;
12
- }): import("react").JSX.Element;
@@ -1,13 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import Link from 'next/link';
3
- /**
4
- * A coverage tile: full-bleed thumbnail on top, then ticker/label, title, summary, and
5
- * prior-report count. Self-contained (not the shared Flowbite Card) so the cover image
6
- * sits flush at the top of every card regardless of how much text each one has — the
7
- * shared card theme centers its content (`justify-center`), which left the covers at
8
- * different heights across a row.
9
- */
10
- export function CoverageCard({ item, hrefBase = '/research', }) {
11
- var _a;
12
- return (_jsx(Link, { href: `${hrefBase}/${item.ticker.toLowerCase()}`, className: "group block h-full", children: _jsxs("div", { className: "flex h-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white/80 shadow-lg backdrop-blur-sm transition-shadow duration-300 hover:shadow-xl dark:border-gray-700 dark:bg-zinc-950", children: [item.assets.thumbnail && (_jsx("img", { src: item.assets.thumbnail, alt: item.title, className: "aspect-video w-full object-cover" })), _jsxs("div", { className: "flex flex-1 flex-col gap-2 p-5", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-gray-500 dark:text-gray-400", children: [_jsx("span", { className: "rounded bg-cyan-500/10 px-2 py-0.5 font-semibold text-cyan-600 dark:text-cyan-400", children: item.ticker }), item.coverage_label && _jsx("span", { children: item.coverage_label }), _jsx("span", { className: "ml-auto", children: (_a = item.date) === null || _a === void 0 ? void 0 : _a.slice(0, 10) })] }), _jsx("h3", { className: "text-lg font-bold text-gray-900 group-hover:text-cyan-600 dark:text-white dark:group-hover:text-cyan-400", children: item.title }), _jsx("p", { className: "line-clamp-3 text-sm text-gray-600 dark:text-gray-300", children: item.summary }), item.history.length > 0 && (_jsxs("p", { className: "mt-auto pt-1 text-xs text-gray-400", children: ["+", item.history.length, " prior", ' ', item.history.length === 1 ? 'report' : 'reports'] }))] })] }) }));
13
- }
@@ -1,6 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- /** Responsive grid of coverage cards. */
3
- export declare function CoverageGrid({ items, hrefBase, }: {
4
- items: CoverageItem[];
5
- hrefBase?: string;
6
- }): import("react").JSX.Element;
@@ -1,9 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { CoverageCard } from './CoverageCard';
3
- /** Responsive grid of coverage cards. */
4
- export function CoverageGrid({ items, hrefBase, }) {
5
- if (!items.length) {
6
- return (_jsx("p", { className: "py-12 text-center text-gray-500 dark:text-gray-400", children: "No coverage yet \u2014 check back soon." }));
7
- }
8
- return (_jsx("div", { className: "grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3", children: items.map((item) => (_jsx(CoverageCard, { item: item, hrefBase: hrefBase }, item.ticker))) }));
9
- }
@@ -1,5 +0,0 @@
1
- import type { CoverageVersion } from './types';
2
- /** The continuing-coverage timeline: prior dated reports with links to their archived assets. */
3
- export declare function CoverageHistory({ history }: {
4
- history: CoverageVersion[];
5
- }): import("react").JSX.Element;
@@ -1,7 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /** The continuing-coverage timeline: prior dated reports with links to their archived assets. */
3
- export function CoverageHistory({ history }) {
4
- if (!history.length)
5
- return null;
6
- return (_jsxs("section", { className: "mt-12 border-t border-gray-200 pt-8 dark:border-gray-800", children: [_jsx("h2", { className: "mb-4 text-xl font-bold text-gray-900 dark:text-white", children: "Coverage history" }), _jsx("ol", { className: "space-y-3", children: history.map((h) => (_jsxs("li", { className: "flex flex-col gap-1 rounded-lg border border-gray-200 p-4 sm:flex-row sm:items-center sm:gap-4 dark:border-gray-800", children: [_jsx("span", { className: "shrink-0 rounded bg-gray-100 px-2 py-0.5 text-xs font-semibold text-gray-600 dark:bg-gray-800 dark:text-gray-300", children: h.version }), _jsxs("span", { className: "flex-1 text-sm text-gray-700 dark:text-gray-200", children: [h.title || 'Prior report', h.legacy_ticker && (_jsxs("em", { className: "text-gray-400", children: [" (as ", h.legacy_ticker, ")"] }))] }), _jsxs("span", { className: "flex gap-3 text-xs", children: [(h.youtube_url || h.assets.video) && (_jsx("a", { href: h.youtube_url || h.assets.video, className: "text-cyan-600 hover:underline dark:text-cyan-400", children: "Video" })), h.assets.podcast_mp3 && (_jsx("a", { href: h.assets.podcast_mp3, className: "text-cyan-600 hover:underline dark:text-cyan-400", children: "Podcast" })), h.assets.brief && (_jsx("a", { href: h.assets.brief, className: "text-cyan-600 hover:underline dark:text-cyan-400", children: "Brief" }))] })] }, h.version))) })] }));
7
- }
@@ -1,16 +0,0 @@
1
- import type { CoverageItem } from './types';
2
- /**
3
- * The full coverage report: native video, the Q&A podcast (surfaced as a prominent
4
- * "Listen" card right under the video), then the brief rendered from markdown (its own
5
- * leading H1 is stripped — we render the title above it), and the continuing-coverage
6
- * history. Works in a server component (SSR'd for SEO) or a client one.
7
- * Theme-aware: readable in both light and dark. The prose body sets an explicit
8
- * light + `dark:` color for every element it renders (headings, p, strong, em, links,
9
- * lists, code, blockquote, hr, tables) rather than relying on `prose-invert` /
10
- * `dark:prose-invert` — the latter doesn't resolve from a `.dark` class in the
11
- * Tailwind v4 + typography-plugin setup these apps use.
12
- */
13
- export declare function ResearchArticle({ item, briefMarkdown, }: {
14
- item: CoverageItem;
15
- briefMarkdown?: string;
16
- }): import("react").JSX.Element;
@@ -1,25 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import ReactMarkdown from 'react-markdown';
3
- import remarkGfm from 'remark-gfm';
4
- import { youtubeId } from './catalog';
5
- import { CoverageHistory } from './CoverageHistory';
6
- /**
7
- * The full coverage report: native video, the Q&A podcast (surfaced as a prominent
8
- * "Listen" card right under the video), then the brief rendered from markdown (its own
9
- * leading H1 is stripped — we render the title above it), and the continuing-coverage
10
- * history. Works in a server component (SSR'd for SEO) or a client one.
11
- * Theme-aware: readable in both light and dark. The prose body sets an explicit
12
- * light + `dark:` color for every element it renders (headings, p, strong, em, links,
13
- * lists, code, blockquote, hr, tables) rather than relying on `prose-invert` /
14
- * `dark:prose-invert` — the latter doesn't resolve from a `.dark` class in the
15
- * Tailwind v4 + typography-plugin setup these apps use.
16
- */
17
- export function ResearchArticle({ item, briefMarkdown, }) {
18
- var _a;
19
- const body = (briefMarkdown || '').replace(/^#\s.*(\r?\n)+/, '');
20
- const ytId = youtubeId(item.youtube_url);
21
- const podcastYtId = youtubeId(item.podcast_youtube_url);
22
- return (_jsxs("article", { className: "mx-auto max-w-3xl", children: [_jsxs("div", { className: "mb-3 flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400", children: [_jsxs("span", { className: "rounded bg-cyan-500/10 px-2 py-0.5 font-semibold text-cyan-600 dark:text-cyan-400", children: [item.company, " \u00B7 ", item.ticker] }), item.coverage_label && _jsx("span", { children: item.coverage_label }), _jsx("span", { children: (_a = item.date) === null || _a === void 0 ? void 0 : _a.slice(0, 10) })] }), _jsx("h1", { className: "mb-6 text-3xl font-bold text-gray-900 dark:text-white", children: item.title }), ytId ? (_jsx("div", { className: "mb-8 aspect-video w-full overflow-hidden rounded-xl bg-black", children: _jsx("iframe", { className: "h-full w-full", src: `https://www.youtube.com/embed/${ytId}`, title: item.title, loading: "lazy", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true }) })) : (item.assets.video && (_jsx("video", { controls: true, poster: item.assets.thumbnail, src: item.assets.video, className: "mb-8 aspect-video w-full rounded-xl bg-black" }))), (item.assets.podcast_mp3 || podcastYtId) && (_jsx("section", { className: "mb-8", children: item.assets.podcast_mp3 ? (_jsxs("div", { className: "rounded-xl border border-cyan-500/30 bg-cyan-50/60 p-4 dark:bg-gray-900/50", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between gap-3", children: [_jsx("p", { className: "text-sm font-semibold text-cyan-600 dark:text-cyan-400", children: "\uD83C\uDF99 Listen \u2014 Q&A podcast" }), podcastYtId && (_jsx("a", { href: item.podcast_youtube_url, target: "_blank", rel: "noopener noreferrer", className: "shrink-0 text-xs text-cyan-600 hover:underline dark:text-cyan-400", children: "Watch on YouTube \u2197" }))] }), _jsx("audio", { controls: true, preload: "none", src: item.assets.podcast_mp3, className: "w-full" })] })) : (
23
- // no MP3 yet — fall back to the YouTube video player
24
- _jsxs(_Fragment, { children: [_jsx("h2", { className: "mb-3 text-xl font-bold text-gray-900 dark:text-white", children: "Listen \u2014 Q&A podcast" }), _jsx("div", { className: "aspect-video w-full overflow-hidden rounded-xl bg-black", children: _jsx("iframe", { className: "h-full w-full", src: `https://www.youtube.com/embed/${podcastYtId}`, title: `${item.title} — Q&A podcast`, loading: "lazy", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true }) })] })) })), body && (_jsx("div", { className: "prose prose-lg prose-headings:font-heading prose-headings:font-bold prose-headings:text-gray-900 dark:prose-headings:text-white prose-p:text-gray-700 dark:prose-p:text-gray-300 prose-p:leading-relaxed prose-a:text-cyan-600 dark:prose-a:text-cyan-400 prose-a:no-underline hover:prose-a:text-cyan-500 dark:hover:prose-a:text-cyan-300 prose-strong:text-gray-900 dark:prose-strong:text-white prose-strong:font-semibold prose-em:text-gray-700 dark:prose-em:text-gray-300 prose-code:text-cyan-700 dark:prose-code:text-cyan-400 prose-code:bg-gray-100 dark:prose-code:bg-gray-800 prose-code:px-2 prose-code:py-1 prose-code:rounded prose-pre:bg-gray-900 prose-pre:border prose-pre:border-gray-800 prose-blockquote:border-l-cyan-500 prose-blockquote:text-gray-600 dark:prose-blockquote:text-gray-400 prose-blockquote:italic prose-ul:text-gray-700 dark:prose-ul:text-gray-300 prose-ol:text-gray-700 dark:prose-ol:text-gray-300 prose-li:marker:text-cyan-500 prose-hr:border-gray-200 dark:prose-hr:border-gray-800 prose-table:border-gray-300 dark:prose-table:border-gray-700 prose-th:bg-gray-100 dark:prose-th:bg-gray-900 prose-th:text-gray-900 dark:prose-th:text-white prose-td:text-gray-700 dark:prose-td:text-gray-300 max-w-none", children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: body }) })), _jsx(CoverageHistory, { history: item.history }), _jsxs("footer", { className: "mt-12 border-t border-gray-200 pt-6 dark:border-gray-800", children: [_jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Audio & music produced with ElevenLabs." }), _jsxs("a", { href: "https://elevenlabs.io/startup-grants", target: "_blank", rel: "noopener noreferrer", "aria-label": "Backed by the ElevenLabs Grants program", className: "mt-3 inline-block", children: [_jsx("img", { src: "/images/logos/elevenlabs-grants.webp", alt: "Backed by the ElevenLabs Grants program", width: 200, className: "h-auto dark:hidden" }), _jsx("img", { src: "/images/logos/elevenlabs-grants-white.webp", alt: "Backed by the ElevenLabs Grants program", width: 200, className: "hidden h-auto dark:block" })] }), _jsxs("p", { className: "mt-2 text-xs text-gray-400 dark:text-gray-500", children: ["Using ElevenLabs yourself? Our", ' ', _jsx("a", { href: "https://try.elevenlabs.io/v9z3wzm97gk3", target: "_blank", rel: "noopener noreferrer", className: "hover:underline", children: "referral link" }), ' ', "costs you nothing extra and supports this research."] })] })] }));
25
- }
@@ -1,14 +0,0 @@
1
- import type { Catalog, CoverageItem } from './types';
2
- export declare const CATALOG_URL: string;
3
- /** Fetch the full catalog. `revalidate` (seconds) drives Next ISR on the server. */
4
- export declare function fetchCatalog(revalidate?: number): Promise<Catalog>;
5
- /** All coverage items, newest first (the catalog is already sorted, but be defensive). */
6
- export declare function getAllCoverage(revalidate?: number): Promise<CoverageItem[]>;
7
- /** One company's coverage (latest + history), or null if not in the catalog. */
8
- export declare function getCoverage(ticker: string, revalidate?: number): Promise<CoverageItem | null>;
9
- /** Tickers for generateStaticParams(). */
10
- export declare function getCoverageTickers(): Promise<string[]>;
11
- /** Fetch a brief's raw markdown (rendered to HTML by ResearchArticle). */
12
- export declare function fetchBrief(url: string, revalidate?: number): Promise<string>;
13
- /** Extract a YouTube video id from a youtu.be / watch?v= / embed URL (or null). */
14
- export declare function youtubeId(url?: string | null): string | null;
@@ -1,42 +0,0 @@
1
- // Data access for the research catalog. The catalog is a single public JSON file served
2
- // via the CloudFront CDN, produced by `just reindex` in robosystems-content-machine.
3
- // Fetched server-side (SSG/ISR) in the public site and client-side in the logged-in app.
4
- export const CATALOG_URL = process.env.NEXT_PUBLIC_RESEARCH_CATALOG_URL ||
5
- 'https://assets.robosystems.ai/content/index.json';
6
- /** Fetch the full catalog. `revalidate` (seconds) drives Next ISR on the server. */
7
- export async function fetchCatalog(revalidate = 3600) {
8
- const res = await fetch(CATALOG_URL, { next: { revalidate } });
9
- if (!res.ok)
10
- throw new Error(`Research catalog fetch failed: ${res.status}`);
11
- return (await res.json());
12
- }
13
- /** All coverage items, newest first (the catalog is already sorted, but be defensive). */
14
- export async function getAllCoverage(revalidate = 3600) {
15
- const { items } = await fetchCatalog(revalidate);
16
- return [...items].sort((a, b) => (b.date || '').localeCompare(a.date || ''));
17
- }
18
- /** One company's coverage (latest + history), or null if not in the catalog. */
19
- export async function getCoverage(ticker, revalidate = 3600) {
20
- const { items } = await fetchCatalog(revalidate);
21
- const t = ticker.toUpperCase();
22
- return items.find((i) => i.ticker.toUpperCase() === t) || null;
23
- }
24
- /** Tickers for generateStaticParams(). */
25
- export async function getCoverageTickers() {
26
- const { items } = await fetchCatalog();
27
- return items.map((i) => i.ticker);
28
- }
29
- /** Fetch a brief's raw markdown (rendered to HTML by ResearchArticle). */
30
- export async function fetchBrief(url, revalidate = 3600) {
31
- const res = await fetch(url, { next: { revalidate } });
32
- if (!res.ok)
33
- throw new Error(`Brief fetch failed: ${res.status}`);
34
- return await res.text();
35
- }
36
- /** Extract a YouTube video id from a youtu.be / watch?v= / embed URL (or null). */
37
- export function youtubeId(url) {
38
- if (!url)
39
- return null;
40
- const m = url.match(/(?:youtu\.be\/|[?&]v=|embed\/)([A-Za-z0-9_-]{6,})/);
41
- return m ? m[1] : null;
42
- }
@@ -1,7 +0,0 @@
1
- export { CATALOG_URL, fetchBrief, fetchCatalog, getAllCoverage, getCoverage, getCoverageTickers, youtubeId, } from './catalog';
2
- export { CoverageBrowser, type CoverageBrowserProps } from './CoverageBrowser';
3
- export { CoverageCard } from './CoverageCard';
4
- export { CoverageGrid } from './CoverageGrid';
5
- export { CoverageHistory } from './CoverageHistory';
6
- export { ResearchArticle } from './ResearchArticle';
7
- export * from './types';
package/research/index.js DELETED
@@ -1,10 +0,0 @@
1
- // Research portal module — shared across robosystems-app (public/SEO) and
2
- // roboinvestor-app (logged-in). Data layer reads the S3 catalog produced by
3
- // robosystems-content-machine; components render the coverage + continuing-coverage thread.
4
- export { CATALOG_URL, fetchBrief, fetchCatalog, getAllCoverage, getCoverage, getCoverageTickers, youtubeId, } from './catalog';
5
- export { CoverageBrowser } from './CoverageBrowser';
6
- export { CoverageCard } from './CoverageCard';
7
- export { CoverageGrid } from './CoverageGrid';
8
- export { CoverageHistory } from './CoverageHistory';
9
- export { ResearchArticle } from './ResearchArticle';
10
- export * from './types';
@@ -1,38 +0,0 @@
1
- export interface CoverageAssets {
2
- video?: string;
3
- short?: string;
4
- podcast_mp3?: string;
5
- podcast_mp4?: string;
6
- brief?: string;
7
- thumbnail?: string;
8
- }
9
- export interface CoverageVersion {
10
- version: string;
11
- date?: string;
12
- title?: string;
13
- legacy_ticker?: string;
14
- youtube_url?: string;
15
- assets: CoverageAssets;
16
- }
17
- export interface CoverageItem {
18
- ticker: string;
19
- company: string;
20
- title: string;
21
- summary: string;
22
- tags: string[];
23
- campaign?: string | null;
24
- campaign_slug?: string | null;
25
- coverage_label?: string | null;
26
- date: string;
27
- version: string;
28
- youtube_url?: string;
29
- short_youtube_url?: string;
30
- podcast_youtube_url?: string;
31
- assets: CoverageAssets;
32
- history: CoverageVersion[];
33
- }
34
- export interface Catalog {
35
- generated: string;
36
- count: number;
37
- items: CoverageItem[];
38
- }
package/research/types.js DELETED
@@ -1,4 +0,0 @@
1
- // Shared types for the equity-research catalog produced by robosystems-content-machine
2
- // (https://assets.robosystems.ai/content/index.json). The company is the durable
3
- // entity; each run is a dated quarterly report version (latest + history[]).
4
- export {};