@gntik-ai/templates 0.0.0-stage → 0.6.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.
- package/dist/index.d.ts +2420 -0
- package/dist/index.js +14520 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -3
- package/src/accept-invitation/Page.test.tsx +40 -0
- package/src/accept-invitation/Page.tsx +153 -0
- package/src/accept-invitation/data.ts +22 -0
- package/src/accept-invitation/template.meta.ts +12 -0
- package/src/assistant-chat/Page.test.tsx +51 -0
- package/src/assistant-chat/Page.tsx +273 -0
- package/src/assistant-chat/data.ts +71 -0
- package/src/assistant-chat/template.meta.ts +12 -0
- package/src/audit-log/Page.test.tsx +58 -0
- package/src/audit-log/Page.tsx +284 -0
- package/src/audit-log/data.ts +133 -0
- package/src/audit-log/template.meta.ts +12 -0
- package/src/board/Lane.tsx +117 -0
- package/src/board/Page.test.tsx +68 -0
- package/src/board/Page.tsx +178 -0
- package/src/board/data.ts +127 -0
- package/src/board/template.meta.ts +12 -0
- package/src/board/useBoard.ts +146 -0
- package/src/changelog/Page.test.tsx +31 -0
- package/src/changelog/Page.tsx +174 -0
- package/src/changelog/data.ts +105 -0
- package/src/changelog/template.meta.ts +12 -0
- package/src/code-editor/Page.test.tsx +83 -0
- package/src/code-editor/Page.tsx +207 -0
- package/src/code-editor/data.ts +130 -0
- package/src/code-editor/template.meta.ts +12 -0
- package/src/contact/Page.test.tsx +33 -0
- package/src/contact/Page.tsx +272 -0
- package/src/contact/PublicFrame.tsx +84 -0
- package/src/contact/data.ts +129 -0
- package/src/contact/template.meta.ts +12 -0
- package/src/create-wizard/Overlay.test.tsx +68 -0
- package/src/create-wizard/Page.test.tsx +57 -0
- package/src/create-wizard/Page.tsx +246 -0
- package/src/create-wizard/data.ts +139 -0
- package/src/create-wizard/examples/Overlay.tsx +60 -0
- package/src/create-wizard/template.meta.ts +12 -0
- package/src/create-workspace/Page.test.tsx +41 -0
- package/src/create-workspace/Page.tsx +227 -0
- package/src/create-workspace/data.ts +61 -0
- package/src/create-workspace/template.meta.ts +12 -0
- package/src/data-explorer/Page.test.tsx +45 -0
- package/src/data-explorer/Page.tsx +247 -0
- package/src/data-explorer/data.ts +94 -0
- package/src/data-explorer/template.meta.ts +12 -0
- package/src/docs-article/ArticleFeedback.tsx +75 -0
- package/src/docs-article/Page.test.tsx +34 -0
- package/src/docs-article/Page.tsx +200 -0
- package/src/docs-article/data.ts +65 -0
- package/src/docs-article/template.meta.ts +12 -0
- package/src/evaluations/Page.test.tsx +40 -0
- package/src/evaluations/Page.tsx +238 -0
- package/src/evaluations/data.ts +194 -0
- package/src/evaluations/template.meta.ts +12 -0
- package/src/forgot-password/Page.test.tsx +199 -0
- package/src/forgot-password/Page.tsx +66 -0
- package/src/forgot-password/data.ts +5 -0
- package/src/forgot-password/examples/UsernameOrEmail.tsx +25 -0
- package/src/forgot-password/template.meta.ts +12 -0
- package/src/getting-started/Page.test.tsx +27 -0
- package/src/getting-started/Page.tsx +125 -0
- package/src/getting-started/data.ts +67 -0
- package/src/getting-started/template.meta.ts +12 -0
- package/src/help-home/Page.test.tsx +33 -0
- package/src/help-home/Page.tsx +159 -0
- package/src/help-home/data.ts +151 -0
- package/src/help-home/template.meta.ts +12 -0
- package/src/home-dashboard/Page.test.tsx +57 -0
- package/src/home-dashboard/Page.tsx +152 -0
- package/src/home-dashboard/data.ts +66 -0
- package/src/home-dashboard/template.meta.ts +12 -0
- package/src/incident-board/Page.test.tsx +77 -0
- package/src/incident-board/Page.tsx +263 -0
- package/src/incident-board/data.ts +220 -0
- package/src/incident-board/template.meta.ts +12 -0
- package/src/incident-console/Page.test.tsx +49 -0
- package/src/incident-console/Page.tsx +274 -0
- package/src/incident-console/data.ts +176 -0
- package/src/incident-console/template.meta.ts +12 -0
- package/src/index.ts +250 -0
- package/src/keyboard-shortcuts/Page.test.tsx +45 -0
- package/src/keyboard-shortcuts/Page.tsx +94 -0
- package/src/keyboard-shortcuts/ShortcutSheet.tsx +80 -0
- package/src/keyboard-shortcuts/data.ts +84 -0
- package/src/keyboard-shortcuts/template.meta.ts +12 -0
- package/src/landing/Page.test.tsx +27 -0
- package/src/landing/Page.tsx +110 -0
- package/src/landing/data.ts +17 -0
- package/src/landing/template.meta.ts +11 -0
- package/src/live-event-stream/Page.test.tsx +251 -0
- package/src/live-event-stream/Page.tsx +307 -0
- package/src/live-event-stream/data.ts +44 -0
- package/src/live-event-stream/examples/Connecting.tsx +5 -0
- package/src/live-event-stream/examples/Error.tsx +5 -0
- package/src/live-event-stream/examples/LiveEmpty.tsx +5 -0
- package/src/live-event-stream/examples/LiveWithEvents.tsx +6 -0
- package/src/live-event-stream/examples/Stopped.tsx +6 -0
- package/src/live-event-stream/template.meta.ts +74 -0
- package/src/messaging/Page.test.tsx +34 -0
- package/src/messaging/Page.tsx +236 -0
- package/src/messaging/data.ts +118 -0
- package/src/messaging/template.meta.ts +12 -0
- package/src/meta.ts +24 -0
- package/src/mfa/Page.test.tsx +22 -0
- package/src/mfa/Page.tsx +45 -0
- package/src/mfa/data.ts +6 -0
- package/src/mfa/template.meta.ts +12 -0
- package/src/model-providers/Page.test.tsx +58 -0
- package/src/model-providers/Page.tsx +198 -0
- package/src/model-providers/data.ts +101 -0
- package/src/model-providers/template.meta.ts +12 -0
- package/src/notification-inbox/Page.test.tsx +49 -0
- package/src/notification-inbox/Page.tsx +237 -0
- package/src/notification-inbox/data.ts +129 -0
- package/src/notification-inbox/template.meta.ts +12 -0
- package/src/pending-activation/Page.test.tsx +390 -0
- package/src/pending-activation/Page.tsx +145 -0
- package/src/pending-activation/data.ts +25 -0
- package/src/pending-activation/template.meta.ts +12 -0
- package/src/pricing/ComparisonTable.tsx +75 -0
- package/src/pricing/Page.test.tsx +29 -0
- package/src/pricing/Page.tsx +83 -0
- package/src/pricing/PublicFrame.tsx +84 -0
- package/src/pricing/data.ts +104 -0
- package/src/pricing/template.meta.ts +12 -0
- package/src/prompt-playground/Page.test.tsx +72 -0
- package/src/prompt-playground/Page.tsx +254 -0
- package/src/prompt-playground/data.ts +68 -0
- package/src/prompt-playground/template.meta.ts +12 -0
- package/src/public-hub/Page.test.tsx +176 -0
- package/src/public-hub/Page.tsx +125 -0
- package/src/public-hub/Source.test.ts +54 -0
- package/src/public-hub/data.ts +24 -0
- package/src/public-hub/examples/Dark.tsx +11 -0
- package/src/public-hub/examples/Full.tsx +6 -0
- package/src/public-hub/examples/HighContrast.tsx +11 -0
- package/src/public-hub/examples/Light.tsx +11 -0
- package/src/public-hub/examples/Loading.tsx +6 -0
- package/src/public-hub/examples/Minimal.tsx +5 -0
- package/src/public-hub/examples/MinimalLoading.tsx +5 -0
- package/src/public-hub/template.meta.ts +60 -0
- package/src/reset-password/Page.test.tsx +27 -0
- package/src/reset-password/Page.tsx +46 -0
- package/src/reset-password/data.ts +5 -0
- package/src/reset-password/template.meta.ts +12 -0
- package/src/resource-detail/Page.test.tsx +260 -0
- package/src/resource-detail/Page.tsx +262 -0
- package/src/resource-detail/data.ts +55 -0
- package/src/resource-detail/examples/postgres-table-detail.tsx +28 -0
- package/src/resource-detail/examples/realtime-configuration.tsx +26 -0
- package/src/resource-detail/examples/runtime-with-transition.tsx +30 -0
- package/src/resource-detail/examples/runtime-without-transition.tsx +33 -0
- package/src/resource-detail/examples/shell.ts +12 -0
- package/src/resource-detail/template.meta.ts +21 -0
- package/src/resource-form/Page.test.tsx +40 -0
- package/src/resource-form/Page.tsx +186 -0
- package/src/resource-form/data.ts +63 -0
- package/src/resource-form/template.meta.ts +12 -0
- package/src/resource-gallery/Page.test.tsx +42 -0
- package/src/resource-gallery/Page.tsx +125 -0
- package/src/resource-gallery/data.ts +44 -0
- package/src/resource-gallery/template.meta.ts +20 -0
- package/src/resource-index/Page.test.tsx +120 -0
- package/src/resource-index/Page.tsx +216 -0
- package/src/resource-index/data.ts +84 -0
- package/src/resource-index/template.meta.ts +21 -0
- package/src/resource-tree/Page.test.tsx +31 -0
- package/src/resource-tree/Page.tsx +232 -0
- package/src/resource-tree/data.ts +180 -0
- package/src/resource-tree/template.meta.ts +12 -0
- package/src/run-trace/Page.test.tsx +56 -0
- package/src/run-trace/Page.tsx +242 -0
- package/src/run-trace/data.ts +171 -0
- package/src/run-trace/template.meta.ts +12 -0
- package/src/scorecard/Page.test.tsx +38 -0
- package/src/scorecard/Page.tsx +160 -0
- package/src/scorecard/TargetsTable.tsx +92 -0
- package/src/scorecard/data.ts +118 -0
- package/src/scorecard/template.meta.ts +12 -0
- package/src/search-results/Page.test.tsx +43 -0
- package/src/search-results/Page.tsx +243 -0
- package/src/search-results/data.ts +130 -0
- package/src/search-results/template.meta.ts +12 -0
- package/src/settings-api-keys/Page.test.tsx +39 -0
- package/src/settings-api-keys/Page.tsx +171 -0
- package/src/settings-api-keys/data.ts +58 -0
- package/src/settings-api-keys/template.meta.ts +12 -0
- package/src/settings-appearance/Page.test.tsx +44 -0
- package/src/settings-appearance/Page.tsx +92 -0
- package/src/settings-appearance/data.ts +21 -0
- package/src/settings-appearance/template.meta.ts +11 -0
- package/src/settings-billing/Page.test.tsx +31 -0
- package/src/settings-billing/Page.tsx +71 -0
- package/src/settings-billing/data.ts +15 -0
- package/src/settings-billing/template.meta.ts +19 -0
- package/src/settings-integrations/Page.test.tsx +31 -0
- package/src/settings-integrations/Page.tsx +185 -0
- package/src/settings-integrations/data.ts +116 -0
- package/src/settings-integrations/template.meta.ts +11 -0
- package/src/settings-members/Page.test.tsx +32 -0
- package/src/settings-members/Page.tsx +140 -0
- package/src/settings-members/data.ts +28 -0
- package/src/settings-members/template.meta.ts +12 -0
- package/src/settings-notifications/Page.test.tsx +31 -0
- package/src/settings-notifications/Page.tsx +71 -0
- package/src/settings-notifications/data.ts +12 -0
- package/src/settings-notifications/template.meta.ts +11 -0
- package/src/settings-profile/Baseline.test.tsx +9 -0
- package/src/settings-profile/Editable.test.tsx +21 -0
- package/src/settings-profile/Page.test.tsx +41 -0
- package/src/settings-profile/Page.tsx +133 -0
- package/src/settings-profile/ReadOnly.test.tsx +143 -0
- package/src/settings-profile/ReadOnlyProfile.tsx +85 -0
- package/src/settings-profile/SettingsFrame.test.tsx +55 -0
- package/src/settings-profile/SettingsFrame.tsx +88 -0
- package/src/settings-profile/__tests__/dom.ts +8 -0
- package/src/settings-profile/data.ts +41 -0
- package/src/settings-profile/examples/ReadOnly.tsx +31 -0
- package/src/settings-profile/fixtures/editable.snap +1 -0
- package/src/settings-profile/settings-nav.ts +98 -0
- package/src/settings-profile/template.meta.ts +21 -0
- package/src/settings-security/MfaSetupDialog.tsx +96 -0
- package/src/settings-security/Page.test.tsx +44 -0
- package/src/settings-security/Page.tsx +158 -0
- package/src/settings-security/data.ts +11 -0
- package/src/settings-security/template.meta.ts +11 -0
- package/src/settings-workspace/Page.test.tsx +44 -0
- package/src/settings-workspace/Page.tsx +120 -0
- package/src/settings-workspace/data.ts +44 -0
- package/src/settings-workspace/template.meta.ts +19 -0
- package/src/shared/ConsoleShell.test.tsx +81 -0
- package/src/shared/ConsoleShell.tsx +98 -0
- package/src/shell-sidebar/Page.test.tsx +21 -0
- package/src/shell-sidebar/Page.tsx +85 -0
- package/src/shell-sidebar/data.ts +24 -0
- package/src/shell-sidebar/template.meta.ts +12 -0
- package/src/shell-topnav/Page.test.tsx +25 -0
- package/src/shell-topnav/Page.tsx +94 -0
- package/src/shell-topnav/data.ts +25 -0
- package/src/shell-topnav/template.meta.ts +12 -0
- package/src/sign-in/BrandPanel.tsx +34 -0
- package/src/sign-in/Page.test.tsx +41 -0
- package/src/sign-in/Page.tsx +73 -0
- package/src/sign-in/data.ts +11 -0
- package/src/sign-in/template.meta.ts +12 -0
- package/src/sign-in-card/Page.test.tsx +182 -0
- package/src/sign-in-card/Page.tsx +82 -0
- package/src/sign-in-card/data.ts +8 -0
- package/src/sign-in-card/examples/Username.tsx +28 -0
- package/src/sign-in-card/template.meta.ts +12 -0
- package/src/sign-up/Page.test.tsx +33 -0
- package/src/sign-up/Page.tsx +70 -0
- package/src/sign-up/data.ts +15 -0
- package/src/sign-up/template.meta.ts +12 -0
- package/src/sso-login/Page.test.tsx +41 -0
- package/src/sso-login/Page.tsx +151 -0
- package/src/sso-login/data.ts +10 -0
- package/src/sso-login/template.meta.ts +12 -0
- package/src/status-403/Page.test.tsx +23 -0
- package/src/status-403/Page.tsx +92 -0
- package/src/status-403/data.ts +13 -0
- package/src/status-403/template.meta.ts +12 -0
- package/src/status-404/Page.test.tsx +23 -0
- package/src/status-404/Page.tsx +60 -0
- package/src/status-404/PageHeading.test.tsx +52 -0
- package/src/status-404/data.ts +12 -0
- package/src/status-404/examples/PageHeader.tsx +28 -0
- package/src/status-404/template.meta.ts +12 -0
- package/src/status-500/Page.test.tsx +24 -0
- package/src/status-500/Page.tsx +70 -0
- package/src/status-500/data.ts +20 -0
- package/src/status-500/template.meta.ts +12 -0
- package/src/status-maintenance/Page.test.tsx +24 -0
- package/src/status-maintenance/Page.tsx +84 -0
- package/src/status-maintenance/data.ts +14 -0
- package/src/status-maintenance/template.meta.ts +12 -0
- package/src/styles.css +11 -0
- package/src/test/a11y.ts +18 -0
- package/src/test/setup.ts +36 -0
- package/src/upgrade-required/Page.test.tsx +28 -0
- package/src/upgrade-required/Page.tsx +79 -0
- package/src/upgrade-required/data.ts +19 -0
- package/src/upgrade-required/template.meta.ts +12 -0
- package/src/usage-analytics/Page.test.tsx +83 -0
- package/src/usage-analytics/Page.tsx +184 -0
- package/src/usage-analytics/data.ts +161 -0
- package/src/usage-analytics/template.meta.ts +21 -0
- package/src/verify-email/Page.test.tsx +32 -0
- package/src/verify-email/Page.tsx +118 -0
- package/src/verify-email/data.ts +7 -0
- package/src/verify-email/template.meta.ts +12 -0
- package/src/workflow-builder/Page.test.tsx +97 -0
- package/src/workflow-builder/Page.tsx +163 -0
- package/src/workflow-builder/data.ts +13 -0
- package/src/workflow-builder/template.meta.ts +12 -0
- package/README.md +0 -3
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Faq,
|
|
3
|
+
FeatureGrid,
|
|
4
|
+
Hero,
|
|
5
|
+
PricingTable,
|
|
6
|
+
type FaqItem,
|
|
7
|
+
type Feature,
|
|
8
|
+
type MarketingAction,
|
|
9
|
+
type PricingCycle,
|
|
10
|
+
type PricingTier,
|
|
11
|
+
} from '@gntik-ai/blocks';
|
|
12
|
+
import { Button, Link, Logo, StackedLayout, ThemeSwitcher, type NavItem } from '@gntik-ai/ui';
|
|
13
|
+
import type { ReactNode } from 'react';
|
|
14
|
+
import { landingCopyright, landingFooterLinks, landingLinks } from './data';
|
|
15
|
+
|
|
16
|
+
export interface LandingProps {
|
|
17
|
+
/** Logo or product name in the navbar. */
|
|
18
|
+
brand: ReactNode;
|
|
19
|
+
links: NavItem[];
|
|
20
|
+
signInHref: string;
|
|
21
|
+
getStartedHref: string;
|
|
22
|
+
heroTitle: ReactNode;
|
|
23
|
+
heroDescription: ReactNode;
|
|
24
|
+
heroPrimaryAction: MarketingAction | null;
|
|
25
|
+
heroSecondaryAction: MarketingAction | null;
|
|
26
|
+
/** Hero screenshot slot; omit for the block placeholder. */
|
|
27
|
+
heroMedia: ReactNode;
|
|
28
|
+
/** Omitted lists use the marketing block samples. */
|
|
29
|
+
features: Feature[];
|
|
30
|
+
tiers: PricingTier[];
|
|
31
|
+
faqs: FaqItem[];
|
|
32
|
+
onSelectTier: (tier: PricingTier, cycle: PricingCycle) => void;
|
|
33
|
+
footerLinks: Array<{ label: string; href: string }>;
|
|
34
|
+
copyright: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Public landing: StackedLayout with Hero, FeatureGrid, PricingTable and Faq. */
|
|
38
|
+
export default function LandingPage(props: Partial<LandingProps>) {
|
|
39
|
+
const {
|
|
40
|
+
brand = <Logo size={26} wordmark />,
|
|
41
|
+
links = landingLinks,
|
|
42
|
+
signInHref = '/sign-in',
|
|
43
|
+
getStartedHref = '/sign-up',
|
|
44
|
+
heroTitle,
|
|
45
|
+
heroDescription,
|
|
46
|
+
heroPrimaryAction = { label: 'Get started', href: getStartedHref },
|
|
47
|
+
heroSecondaryAction,
|
|
48
|
+
heroMedia,
|
|
49
|
+
features,
|
|
50
|
+
tiers,
|
|
51
|
+
faqs,
|
|
52
|
+
onSelectTier,
|
|
53
|
+
footerLinks = landingFooterLinks,
|
|
54
|
+
copyright = landingCopyright,
|
|
55
|
+
} = props;
|
|
56
|
+
return (
|
|
57
|
+
<StackedLayout
|
|
58
|
+
fullScreen
|
|
59
|
+
width="wide"
|
|
60
|
+
brand={brand}
|
|
61
|
+
links={links}
|
|
62
|
+
navLabel="Primary"
|
|
63
|
+
mobileTitle="Menu"
|
|
64
|
+
actions={
|
|
65
|
+
<>
|
|
66
|
+
<ThemeSwitcher size="sm" className="hidden md:inline-flex" />
|
|
67
|
+
<Button variant="ghost" size="sm" render={<a href={signInHref} />} nativeButton={false}>
|
|
68
|
+
Sign in
|
|
69
|
+
</Button>
|
|
70
|
+
<Button size="sm" render={<a href={getStartedHref} />} nativeButton={false}>
|
|
71
|
+
Get started
|
|
72
|
+
</Button>
|
|
73
|
+
</>
|
|
74
|
+
}
|
|
75
|
+
footer={
|
|
76
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
77
|
+
<span>{copyright}</span>
|
|
78
|
+
<nav aria-label="Footer">
|
|
79
|
+
<ul className="flex flex-wrap gap-x-4 gap-y-1">
|
|
80
|
+
{footerLinks.map((l) => (
|
|
81
|
+
<li key={l.href}>
|
|
82
|
+
<Link href={l.href} tone="muted" underline="hover">
|
|
83
|
+
{l.label}
|
|
84
|
+
</Link>
|
|
85
|
+
</li>
|
|
86
|
+
))}
|
|
87
|
+
</ul>
|
|
88
|
+
</nav>
|
|
89
|
+
</div>
|
|
90
|
+
}
|
|
91
|
+
>
|
|
92
|
+
<Hero
|
|
93
|
+
title={heroTitle}
|
|
94
|
+
description={heroDescription}
|
|
95
|
+
primaryAction={heroPrimaryAction}
|
|
96
|
+
secondaryAction={heroSecondaryAction}
|
|
97
|
+
media={heroMedia}
|
|
98
|
+
/>
|
|
99
|
+
<div id="features" className="scroll-mt-20">
|
|
100
|
+
<FeatureGrid features={features} />
|
|
101
|
+
</div>
|
|
102
|
+
<div id="pricing" className="scroll-mt-20">
|
|
103
|
+
<PricingTable tiers={tiers} onSelectTier={onSelectTier} />
|
|
104
|
+
</div>
|
|
105
|
+
<div id="faq" className="scroll-mt-20">
|
|
106
|
+
<Faq items={faqs} />
|
|
107
|
+
</div>
|
|
108
|
+
</StackedLayout>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NavItem } from '@gntik-ai/ui';
|
|
2
|
+
|
|
3
|
+
/** Neutral fixtures for the landing page; sections reuse the marketing block samples. */
|
|
4
|
+
export const landingLinks: NavItem[] = [
|
|
5
|
+
{ label: 'Features', href: '#features' },
|
|
6
|
+
{ label: 'Pricing', href: '#pricing' },
|
|
7
|
+
{ label: 'FAQ', href: '#faq' },
|
|
8
|
+
{ label: 'Docs', href: '/docs' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export const landingFooterLinks = [
|
|
12
|
+
{ label: 'Status', href: '/status' },
|
|
13
|
+
{ label: 'Privacy', href: '/privacy' },
|
|
14
|
+
{ label: 'Terms', href: '/terms' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const landingCopyright = `© ${new Date().getFullYear()} Example, Inc.`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TemplateMeta } from '../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: TemplateMeta = {
|
|
4
|
+
name: 'Landing',
|
|
5
|
+
family: 'Overview',
|
|
6
|
+
priority: 'P3',
|
|
7
|
+
status: 'experimental',
|
|
8
|
+
description: 'Public landing: Hero, FeatureGrid, PricingTable, Faq',
|
|
9
|
+
layout: 'StackedLayout',
|
|
10
|
+
blocks: ['hero', 'feature-grid', 'pricing-table', 'faq'],
|
|
11
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { render, screen, within } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { ThemeProvider } from '@gntik-ai/ui';
|
|
4
|
+
import { expectNoAxeViolations } from '../test/a11y';
|
|
5
|
+
import LiveEventStreamPage from './Page';
|
|
6
|
+
import Connecting from './examples/Connecting';
|
|
7
|
+
import LiveEmpty from './examples/LiveEmpty';
|
|
8
|
+
import LiveWithEvents from './examples/LiveWithEvents';
|
|
9
|
+
import ErrorExample from './examples/Error';
|
|
10
|
+
import Stopped from './examples/Stopped';
|
|
11
|
+
|
|
12
|
+
const originalMatchMedia = window.matchMedia;
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
window.matchMedia = (query: string) =>
|
|
15
|
+
({ ...originalMatchMedia(query), matches: query.includes('min-width') }) as MediaQueryList;
|
|
16
|
+
});
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
window.matchMedia = originalMatchMedia;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const events = [
|
|
22
|
+
{ id: 'a', timestamp: '12:00:00', type: 'created', summary: 'First record', payload: { name: 'alpha' } },
|
|
23
|
+
{ id: 'b', timestamp: '12:00:01', type: 'updated', summary: 'Second record', payload: { name: 'beta' } },
|
|
24
|
+
{ id: 'c', timestamp: '12:00:02', type: 'created', summary: 'Third record', payload: { name: 'gamma' } },
|
|
25
|
+
{ id: 'd', timestamp: '12:00:03', type: 'updated', summary: 'Fourth record', payload: { name: 'delta' } },
|
|
26
|
+
{ id: 'e', timestamp: '12:00:04', type: 'deleted', summary: 'Fifth record', payload: { name: 'epsilon' } },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
describe('LiveEventStreamPage', () => {
|
|
30
|
+
for (const theme of ['dark', 'light', 'high_contrast'] as const) {
|
|
31
|
+
for (const [name, Example] of [
|
|
32
|
+
['connecting', Connecting],
|
|
33
|
+
['live-empty', LiveEmpty],
|
|
34
|
+
['live-with-events', LiveWithEvents],
|
|
35
|
+
['error', ErrorExample],
|
|
36
|
+
['stopped', Stopped],
|
|
37
|
+
['idle', LiveEventStreamPage],
|
|
38
|
+
] as const) {
|
|
39
|
+
it(`${name} is axe-clean in ${theme}`, { timeout: 15000 }, async () => {
|
|
40
|
+
render(
|
|
41
|
+
<ThemeProvider defaultMode={theme} storageKey={null}>
|
|
42
|
+
<Example />
|
|
43
|
+
</ThemeProvider>,
|
|
44
|
+
);
|
|
45
|
+
if (name === 'live-with-events')
|
|
46
|
+
await userEvent.click(screen.getAllByRole('button', { name: /^Inspect event/ })[0]!);
|
|
47
|
+
await expectNoAxeViolations();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
it('defaults to a 500-event buffer and supports an explicitly empty buffer', () => {
|
|
53
|
+
const many = Array.from({ length: 502 }, (_, index) => ({
|
|
54
|
+
id: String(index),
|
|
55
|
+
timestamp: String(index),
|
|
56
|
+
type: 'created',
|
|
57
|
+
payload: null,
|
|
58
|
+
}));
|
|
59
|
+
const { rerender } = render(<LiveEventStreamPage status="live" events={many} />);
|
|
60
|
+
expect(screen.getByText('500 of 502 events buffered')).toBeInTheDocument();
|
|
61
|
+
expect(screen.getByText('500 of 500')).toBeInTheDocument();
|
|
62
|
+
expect(screen.getByRole('button', { name: 'Inspect event 501' })).toBeInTheDocument();
|
|
63
|
+
expect(screen.queryByRole('button', { name: 'Inspect event 0' })).toBeNull();
|
|
64
|
+
rerender(<LiveEventStreamPage status="live" events={many} maxEvents={0} />);
|
|
65
|
+
expect(screen.getByText('0 of 502 events buffered')).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByRole('heading', { name: 'Listening for events' })).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('announces only a consumer-throttled count when one is explicitly provided', () => {
|
|
70
|
+
const { rerender } = render(
|
|
71
|
+
<LiveEventStreamPage status="live" events={events.slice(0, 1)} countAnnouncement="1 event received" />,
|
|
72
|
+
);
|
|
73
|
+
const announcement = screen.getByRole('status', { name: 'Connection status' });
|
|
74
|
+
expect(announcement).toHaveTextContent('Live. 1 event received');
|
|
75
|
+
rerender(<LiveEventStreamPage status="live" events={events} countAnnouncement="1 event received" />);
|
|
76
|
+
expect(announcement).toHaveTextContent('Live. 1 event received');
|
|
77
|
+
rerender(<LiveEventStreamPage status="live" events={events} countAnnouncement="5 events received" />);
|
|
78
|
+
expect(announcement).toHaveTextContent('Live. 5 events received');
|
|
79
|
+
expect(document.querySelectorAll('[aria-live="polite"]')).toHaveLength(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('updates a pinned inspector and preserves its last payload when that event is evicted', async () => {
|
|
83
|
+
const user = userEvent.setup();
|
|
84
|
+
const { rerender } = render(<LiveEventStreamPage status="live" events={events.slice(0, 2)} maxEvents={2} />);
|
|
85
|
+
await user.click(screen.getByRole('button', { name: 'Inspect event 12:00:00' }));
|
|
86
|
+
await user.click(screen.getByRole('button', { name: 'Pin panel' }));
|
|
87
|
+
expect(screen.getByRole('button', { name: 'Pin panel' })).toHaveAttribute('aria-pressed', 'true');
|
|
88
|
+
await user.click(screen.getByRole('button', { name: 'Inspect event 12:00:01' }));
|
|
89
|
+
const panel = screen.getByRole('complementary', { name: 'Event inspector' });
|
|
90
|
+
expect(within(panel).getByRole('tree', { name: 'Event payload' })).toHaveTextContent('beta');
|
|
91
|
+
rerender(<LiveEventStreamPage status="live" events={events} maxEvents={2} />);
|
|
92
|
+
expect(within(panel).getByRole('tree', { name: 'Event payload' })).toHaveTextContent('beta');
|
|
93
|
+
expect(within(panel).getByText('This event is no longer in the buffer.')).toBeInTheDocument();
|
|
94
|
+
panel.focus();
|
|
95
|
+
await user.keyboard('{Escape}');
|
|
96
|
+
expect(panel).toBeInTheDocument();
|
|
97
|
+
await user.click(screen.getByRole('button', { name: 'Close panel' }));
|
|
98
|
+
expect(screen.queryByRole('complementary', { name: 'Event inspector' })).toBeNull();
|
|
99
|
+
expect(screen.getByRole('button', { name: 'Toggle event inspector' })).toHaveFocus();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('closes an unpinned inspector with Escape and returns focus to its toggle', async () => {
|
|
103
|
+
const user = userEvent.setup();
|
|
104
|
+
render(<LiveEventStreamPage status="live" events={events.slice(0, 1)} />);
|
|
105
|
+
await user.click(screen.getByRole('button', { name: 'Inspect event 12:00:00' }));
|
|
106
|
+
const panel = screen.getByRole('complementary', { name: 'Event inspector' });
|
|
107
|
+
panel.focus();
|
|
108
|
+
await user.keyboard('{Escape}');
|
|
109
|
+
expect(screen.queryByRole('complementary', { name: 'Event inspector' })).toBeNull();
|
|
110
|
+
expect(screen.getByRole('button', { name: 'Toggle event inspector' })).toHaveFocus();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('selects with a mouse or Tab and Enter and shows the selected payload', async () => {
|
|
114
|
+
const user = userEvent.setup();
|
|
115
|
+
const onSelectEvent = vi.fn();
|
|
116
|
+
render(<LiveEventStreamPage status="live" events={events.slice(0, 2)} onSelectEvent={onSelectEvent} />);
|
|
117
|
+
await user.click(screen.getByText('First record'));
|
|
118
|
+
expect(screen.getByRole('button', { name: 'Inspect event 12:00:00' })).toHaveAttribute('aria-pressed', 'true');
|
|
119
|
+
const panel = screen.getByRole('complementary', { name: 'Event inspector' });
|
|
120
|
+
expect(within(panel).getByRole('tree', { name: 'Event payload' })).toHaveTextContent('alpha');
|
|
121
|
+
expect(onSelectEvent).toHaveBeenLastCalledWith(events[0]);
|
|
122
|
+
screen.getByRole('button', { name: 'Copy line 12:00:00' }).focus();
|
|
123
|
+
await user.tab();
|
|
124
|
+
expect(screen.getByRole('button', { name: 'Inspect event 12:00:01' })).toHaveFocus();
|
|
125
|
+
await user.keyboard('{Enter}');
|
|
126
|
+
expect(screen.getByRole('button', { name: 'Inspect event 12:00:01' })).toHaveAttribute('aria-pressed', 'true');
|
|
127
|
+
expect(within(panel).getByRole('tree', { name: 'Event payload' })).toHaveTextContent('beta');
|
|
128
|
+
expect(onSelectEvent).toHaveBeenLastCalledWith(events[1]);
|
|
129
|
+
await expectNoAxeViolations();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('filters buffered events by type and retains LogViewer search and copying', async () => {
|
|
133
|
+
const user = userEvent.setup();
|
|
134
|
+
render(
|
|
135
|
+
<LiveEventStreamPage
|
|
136
|
+
status="live"
|
|
137
|
+
events={events}
|
|
138
|
+
maxEvents={3}
|
|
139
|
+
eventTypes={['created', 'updated', 'deleted']}
|
|
140
|
+
/>,
|
|
141
|
+
);
|
|
142
|
+
const types = screen.getByRole('group', { name: 'Event types' });
|
|
143
|
+
await user.click(within(types).getByRole('button', { name: 'created' }));
|
|
144
|
+
expect(screen.getByRole('log')).not.toHaveTextContent('Third record');
|
|
145
|
+
expect(screen.getByRole('log')).toHaveTextContent('Fourth record');
|
|
146
|
+
await user.type(screen.getByRole('searchbox', { name: 'Search logs' }), 'fourth');
|
|
147
|
+
expect(screen.getByText('1 of 2')).toBeInTheDocument();
|
|
148
|
+
await user.click(screen.getByRole('button', { name: 'Copy line 12:00:03' }));
|
|
149
|
+
expect(await navigator.clipboard.readText()).toBe('12:00:03 INFO [updated] Fourth record');
|
|
150
|
+
expect(screen.getByRole('button', { name: 'Copied' })).toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('keeps the newest bounded events in arrival order without announcing additions, including when stopped', () => {
|
|
154
|
+
const { rerender } = render(<LiveEventStreamPage status="live" events={events.slice(0, 3)} maxEvents={3} />);
|
|
155
|
+
const announcement = screen.getByRole('status', { name: 'Connection status' });
|
|
156
|
+
expect(announcement).toHaveTextContent(/^Live$/);
|
|
157
|
+
rerender(<LiveEventStreamPage status="live" events={events} maxEvents={3} />);
|
|
158
|
+
const log = screen.getByRole('log', { name: 'Events' });
|
|
159
|
+
expect(log).toHaveAttribute('aria-live', 'off');
|
|
160
|
+
expect(announcement).toHaveTextContent(/^Live$/);
|
|
161
|
+
expect(screen.getByText('3 of 5 events buffered')).toBeInTheDocument();
|
|
162
|
+
expect(within(log).queryByText('First record')).toBeNull();
|
|
163
|
+
expect(within(log).queryByText('Second record')).toBeNull();
|
|
164
|
+
expect(
|
|
165
|
+
within(log)
|
|
166
|
+
.getAllByRole('button', { name: /^Copy line/ })
|
|
167
|
+
.map((button) => button.getAttribute('aria-label')),
|
|
168
|
+
).toEqual(['Copy line 12:00:02', 'Copy line 12:00:03', 'Copy line 12:00:04']);
|
|
169
|
+
rerender(<LiveEventStreamPage status="stopped" events={events} maxEvents={3} />);
|
|
170
|
+
expect(screen.getByRole('log')).toHaveTextContent('Third record');
|
|
171
|
+
expect(announcement).toHaveTextContent(/^Stopped$/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('shows idle and listening empty states, connecting skeletons, validation and stream errors', async () => {
|
|
175
|
+
const { rerender } = render(<LiveEventStreamPage events={[]} />);
|
|
176
|
+
expect(screen.getByRole('heading', { name: 'No events yet' })).toBeInTheDocument();
|
|
177
|
+
rerender(<LiveEventStreamPage status="live" events={[]} />);
|
|
178
|
+
expect(screen.getByRole('heading', { name: 'Listening for events' })).toBeInTheDocument();
|
|
179
|
+
rerender(<LiveEventStreamPage status="connecting" events={[]} />);
|
|
180
|
+
expect(screen.getByRole('region', { name: 'Events' })).toHaveAttribute('aria-busy', 'true');
|
|
181
|
+
expect(screen.queryByRole('log')).toBeNull();
|
|
182
|
+
expect(
|
|
183
|
+
within(screen.getByRole('region', { name: 'Events' })).getByRole('status', { hidden: true }),
|
|
184
|
+
).toHaveTextContent('Connecting');
|
|
185
|
+
rerender(
|
|
186
|
+
<LiveEventStreamPage status="error" error="Source is unavailable." validationMessage="Choose a source." />,
|
|
187
|
+
);
|
|
188
|
+
expect(screen.getByRole('alert', { name: 'Check subscription parameters' })).toHaveTextContent('Choose a source.');
|
|
189
|
+
expect(screen.getByRole('alert', { name: 'Subscription error' })).toHaveTextContent('Source is unavailable.');
|
|
190
|
+
await expectNoAxeViolations();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('reflects controlled connection states and announces changes independently of event counts', async () => {
|
|
194
|
+
const user = userEvent.setup();
|
|
195
|
+
const onSubscribe = vi.fn();
|
|
196
|
+
const onStop = vi.fn();
|
|
197
|
+
const { rerender } = render(<LiveEventStreamPage onSubscribe={onSubscribe} onStop={onStop} />);
|
|
198
|
+
const announcement = screen.getByRole('status', { name: 'Connection status' });
|
|
199
|
+
for (const [status, label, tone, action, pulse] of [
|
|
200
|
+
['idle', 'Not subscribed', 'neutral', 'Subscribe', false],
|
|
201
|
+
['connecting', 'Connecting', 'info', 'Subscribe', true],
|
|
202
|
+
['live', 'Live', 'success', 'Stop', true],
|
|
203
|
+
['error', 'Error', 'destructive', 'Subscribe', false],
|
|
204
|
+
['stopped', 'Stopped', 'warning', 'Subscribe', false],
|
|
205
|
+
] as const) {
|
|
206
|
+
rerender(<LiveEventStreamPage status={status} onSubscribe={onSubscribe} onStop={onStop} />);
|
|
207
|
+
const indicator = screen.getByText(label, { selector: '[data-tone] > span' }).closest('[data-tone]')!;
|
|
208
|
+
expect(indicator).toHaveAttribute('data-tone', tone);
|
|
209
|
+
expect(Boolean(indicator.querySelector('[data-pulse]'))).toBe(pulse);
|
|
210
|
+
if (pulse)
|
|
211
|
+
expect(indicator.querySelector('[data-pulse]')).toHaveClass(
|
|
212
|
+
'motion-reduce:hidden',
|
|
213
|
+
'motion-reduce:animate-none',
|
|
214
|
+
);
|
|
215
|
+
expect(announcement).toHaveAttribute('aria-live', 'polite');
|
|
216
|
+
expect(announcement).toHaveTextContent(label);
|
|
217
|
+
const button = screen.getByRole('button', { name: action });
|
|
218
|
+
if (status === 'connecting') {
|
|
219
|
+
expect(button).toBeDisabled();
|
|
220
|
+
await user.click(button);
|
|
221
|
+
expect(onSubscribe).toHaveBeenCalledTimes(1);
|
|
222
|
+
} else {
|
|
223
|
+
expect(button).toBeEnabled();
|
|
224
|
+
await user.click(button);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
expect(onSubscribe).toHaveBeenCalledTimes(3);
|
|
228
|
+
expect(onStop).toHaveBeenCalledOnce();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('composes a subscription form, quiet log and complementary inspector without run telemetry', async () => {
|
|
232
|
+
const user = userEvent.setup();
|
|
233
|
+
render(<LiveEventStreamPage form={<p>Consumer subscription fields</p>} />);
|
|
234
|
+
expect(screen.getByRole('main')).toBeInTheDocument();
|
|
235
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Live event stream' })).toBeInTheDocument();
|
|
236
|
+
expect(
|
|
237
|
+
within(screen.getByRole('region', { name: 'Subscription parameters' })).getByText('Consumer subscription fields'),
|
|
238
|
+
).toBeInTheDocument();
|
|
239
|
+
expect(screen.getByRole('button', { name: 'Subscribe' })).toBeInTheDocument();
|
|
240
|
+
expect(screen.getByText('Not subscribed', { selector: 'span' })).toBeInTheDocument();
|
|
241
|
+
expect(screen.getByRole('log', { name: 'Events' })).toHaveAttribute('aria-live', 'off');
|
|
242
|
+
expect(screen.queryByRole('timer')).toBeNull();
|
|
243
|
+
expect(screen.queryByRole('heading', { name: 'Spans' })).toBeNull();
|
|
244
|
+
expect(screen.queryByRole('heading', { name: 'Tokens and cost' })).toBeNull();
|
|
245
|
+
await user.click(screen.getByRole('button', { name: 'Toggle event inspector' }));
|
|
246
|
+
expect(screen.getByRole('complementary', { name: 'Event inspector' })).toHaveTextContent(
|
|
247
|
+
'Select an event to inspect its payload.',
|
|
248
|
+
);
|
|
249
|
+
await expectNoAxeViolations();
|
|
250
|
+
});
|
|
251
|
+
});
|