@gntik-ai/templates 0.0.0-stage → 0.5.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,81 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import type { ComponentType } from 'react';
|
|
4
|
+
import AssistantChatPage from '../assistant-chat/Page';
|
|
5
|
+
import AuditLogPage from '../audit-log/Page';
|
|
6
|
+
import GettingStartedPage from '../getting-started/Page';
|
|
7
|
+
import NotificationInboxPage from '../notification-inbox/Page';
|
|
8
|
+
import SearchResultsPage from '../search-results/Page';
|
|
9
|
+
import ShellSidebarPage from '../shell-sidebar/Page';
|
|
10
|
+
import UpgradeRequiredPage from '../upgrade-required/Page';
|
|
11
|
+
import { ConsoleShell, type ConsoleShellProps } from './ConsoleShell';
|
|
12
|
+
|
|
13
|
+
const workspaces = [
|
|
14
|
+
{ id: 'ws_a', name: 'Alpha', plan: 'Pro' },
|
|
15
|
+
{ id: 'ws_b', name: 'Beta', plan: 'Free' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
describe('ConsoleShell', () => {
|
|
19
|
+
it('shows the default usage meter and help link', () => {
|
|
20
|
+
render(<ConsoleShell>content</ConsoleShell>);
|
|
21
|
+
expect(screen.getByRole('meter')).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByRole('link', { name: 'Help & docs' })).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('forwards the sidebar header, footer and layout options', { timeout: 15000 }, async () => {
|
|
26
|
+
const onSignOut = vi.fn();
|
|
27
|
+
const onWorkspaceChange = vi.fn();
|
|
28
|
+
render(
|
|
29
|
+
<ConsoleShell
|
|
30
|
+
sidebarLabel="Product sidebar"
|
|
31
|
+
workspaces={workspaces}
|
|
32
|
+
currentWorkspaceId="ws_b"
|
|
33
|
+
onWorkspaceChange={onWorkspaceChange}
|
|
34
|
+
usage={{ label: 'Agent runs', value: 40, max: 100, valueLabel: '40 / 100' }}
|
|
35
|
+
helpHref="/docs"
|
|
36
|
+
helpLabel="Documentation"
|
|
37
|
+
user={{ name: 'Ada Lovelace', email: 'ada@example.com' }}
|
|
38
|
+
userMenuItems={[{ label: 'Billing portal', onSelect: () => {} }]}
|
|
39
|
+
onSignOut={onSignOut}
|
|
40
|
+
>
|
|
41
|
+
content
|
|
42
|
+
</ConsoleShell>,
|
|
43
|
+
);
|
|
44
|
+
expect(screen.getByRole('complementary', { name: 'Product sidebar' })).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByRole('meter', { name: 'Agent runs' })).toBeInTheDocument();
|
|
46
|
+
expect(screen.getByRole('link', { name: 'Documentation' })).toHaveAttribute('href', '/docs');
|
|
47
|
+
expect(screen.getAllByText('Beta').length).toBeGreaterThan(0);
|
|
48
|
+
const [trigger] = screen.getAllByRole('button', { name: /Ada Lovelace/ });
|
|
49
|
+
await userEvent.click(trigger!);
|
|
50
|
+
expect(await screen.findByRole('menuitem', { name: 'Billing portal' })).toBeInTheDocument();
|
|
51
|
+
await userEvent.click(screen.getByRole('menuitem', { name: /Sign out/ }));
|
|
52
|
+
expect(onSignOut).toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('`null` hides the usage meter and the help link', () => {
|
|
56
|
+
render(
|
|
57
|
+
<ConsoleShell usage={null} helpHref={null}>
|
|
58
|
+
content
|
|
59
|
+
</ConsoleShell>,
|
|
60
|
+
);
|
|
61
|
+
expect(screen.queryByRole('meter')).toBeNull();
|
|
62
|
+
expect(screen.queryByRole('link', { name: 'Help & docs' })).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const pages: Array<[string, ComponentType<{ shell?: Omit<ConsoleShellProps, 'children'> }>]> = [
|
|
66
|
+
['AssistantChatPage', AssistantChatPage],
|
|
67
|
+
['AuditLogPage', AuditLogPage],
|
|
68
|
+
['GettingStartedPage', GettingStartedPage],
|
|
69
|
+
['NotificationInboxPage', NotificationInboxPage],
|
|
70
|
+
['SearchResultsPage', SearchResultsPage],
|
|
71
|
+
['ShellSidebarPage', ShellSidebarPage],
|
|
72
|
+
['UpgradeRequiredPage', UpgradeRequiredPage],
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
it.each(pages)('%s forwards its `shell` prop', { timeout: 15000 }, (_, Page) => {
|
|
76
|
+
render(<Page shell={{ sidebarLabel: 'Custom sidebar', usage: null, helpHref: null }} />);
|
|
77
|
+
expect(screen.getByRole('complementary', { name: 'Custom sidebar' })).toBeInTheDocument();
|
|
78
|
+
expect(screen.queryByRole('meter')).toBeNull();
|
|
79
|
+
expect(screen.queryByRole('link', { name: 'Help & docs' })).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppSidebarFooter,
|
|
3
|
+
AppSidebarHeader,
|
|
4
|
+
AppSidebarNav,
|
|
5
|
+
AppTopbar,
|
|
6
|
+
type AppSidebarFooterProps,
|
|
7
|
+
type AppSidebarHeaderProps,
|
|
8
|
+
type AppTopbarProps,
|
|
9
|
+
} from '@gntik-ai/blocks';
|
|
10
|
+
import {
|
|
11
|
+
SidebarLayout,
|
|
12
|
+
type BreadcrumbItem,
|
|
13
|
+
type NavGroup,
|
|
14
|
+
type SidebarLayoutProps,
|
|
15
|
+
type UserMenuUser,
|
|
16
|
+
type Workspace,
|
|
17
|
+
} from '@gntik-ai/ui';
|
|
18
|
+
import type { ReactNode } from 'react';
|
|
19
|
+
|
|
20
|
+
export interface ConsoleShellProps
|
|
21
|
+
extends
|
|
22
|
+
Pick<AppSidebarHeaderProps, 'currentWorkspaceId' | 'onWorkspaceChange' | 'onCreateWorkspace'>,
|
|
23
|
+
Pick<AppSidebarFooterProps, 'usage' | 'helpHref' | 'helpLabel' | 'userMenuItems' | 'onSignOut'>,
|
|
24
|
+
Pick<SidebarLayoutProps, 'sidebarLabel' | 'collapseMode'> {
|
|
25
|
+
/** Page content (usually a <Page>). */
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
/** Sidebar navigation; defaults to the AppSidebar sample groups. */
|
|
28
|
+
nav?: NavGroup[];
|
|
29
|
+
/** Href of the current page, marks the active nav item. */
|
|
30
|
+
currentHref?: string;
|
|
31
|
+
breadcrumbs?: BreadcrumbItem[] | null;
|
|
32
|
+
workspaces?: Workspace[] | null;
|
|
33
|
+
user?: UserMenuUser | null;
|
|
34
|
+
/** Extra AppTopbar props (environment, notifications, theme control…). */
|
|
35
|
+
topbar?: Omit<AppTopbarProps, 'breadcrumbs' | 'user'>;
|
|
36
|
+
/** localStorage key for the collapsed sidebar; `undefined` disables persistence. */
|
|
37
|
+
storageKey?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The console frame shared by the templates: SidebarLayout with the AppSidebar header, nav and
|
|
42
|
+
* footer, and the AppTopbar (breadcrumb, ⌘K search, notifications, theme, user menu).
|
|
43
|
+
* Templates fill it with a Page; products swap the nav, workspaces, user, usage meter, help
|
|
44
|
+
* link and user-menu items.
|
|
45
|
+
*/
|
|
46
|
+
export function ConsoleShell({
|
|
47
|
+
children,
|
|
48
|
+
nav,
|
|
49
|
+
currentHref,
|
|
50
|
+
breadcrumbs,
|
|
51
|
+
workspaces,
|
|
52
|
+
currentWorkspaceId,
|
|
53
|
+
onWorkspaceChange,
|
|
54
|
+
onCreateWorkspace,
|
|
55
|
+
user,
|
|
56
|
+
usage,
|
|
57
|
+
helpHref,
|
|
58
|
+
helpLabel,
|
|
59
|
+
userMenuItems,
|
|
60
|
+
onSignOut,
|
|
61
|
+
topbar,
|
|
62
|
+
storageKey,
|
|
63
|
+
sidebarLabel,
|
|
64
|
+
collapseMode,
|
|
65
|
+
}: ConsoleShellProps) {
|
|
66
|
+
return (
|
|
67
|
+
<SidebarLayout
|
|
68
|
+
fullScreen
|
|
69
|
+
storageKey={storageKey}
|
|
70
|
+
sidebarLabel={sidebarLabel}
|
|
71
|
+
collapseMode={collapseMode}
|
|
72
|
+
sidebarHeader={({ collapsed }) => (
|
|
73
|
+
<AppSidebarHeader
|
|
74
|
+
collapsed={collapsed}
|
|
75
|
+
workspaces={workspaces}
|
|
76
|
+
currentWorkspaceId={currentWorkspaceId}
|
|
77
|
+
onWorkspaceChange={onWorkspaceChange}
|
|
78
|
+
onCreateWorkspace={onCreateWorkspace}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
sidebar={({ collapsed }) => <AppSidebarNav groups={nav} currentHref={currentHref} collapsed={collapsed} />}
|
|
82
|
+
sidebarFooter={({ collapsed }) => (
|
|
83
|
+
<AppSidebarFooter
|
|
84
|
+
collapsed={collapsed}
|
|
85
|
+
user={user}
|
|
86
|
+
usage={usage}
|
|
87
|
+
helpHref={helpHref}
|
|
88
|
+
helpLabel={helpLabel}
|
|
89
|
+
userMenuItems={userMenuItems}
|
|
90
|
+
onSignOut={onSignOut}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
93
|
+
topbar={<AppTopbar breadcrumbs={breadcrumbs} user={user} {...topbar} />}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</SidebarLayout>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../test/a11y';
|
|
4
|
+
import ShellSidebarPage from './Page';
|
|
5
|
+
|
|
6
|
+
describe('ShellSidebarPage', () => {
|
|
7
|
+
it('renders the console frame with an empty page', { timeout: 15000 }, async () => {
|
|
8
|
+
const { container } = render(<ShellSidebarPage />);
|
|
9
|
+
expect(screen.getByRole('main')).toBeInTheDocument();
|
|
10
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Projects' })).toBeInTheDocument();
|
|
11
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Create your first project' })).toBeInTheDocument();
|
|
12
|
+
await expectNoAxeViolations(container);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('runs the first-run action', { timeout: 15000 }, async () => {
|
|
16
|
+
const onCreate = vi.fn();
|
|
17
|
+
render(<ShellSidebarPage onCreate={onCreate} />);
|
|
18
|
+
await userEvent.click(screen.getByRole('button', { name: 'New project' }));
|
|
19
|
+
expect(onCreate).toHaveBeenCalledOnce();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { FirstRunEmpty, PageHeader } from '@gntik-ai/blocks';
|
|
2
|
+
import { Page, type BreadcrumbItem, type NavGroup, type UserMenuUser, type Workspace } from '@gntik-ai/ui';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
|
|
5
|
+
import {
|
|
6
|
+
shellSidebarBreadcrumbs,
|
|
7
|
+
shellSidebarCopy,
|
|
8
|
+
shellSidebarNav,
|
|
9
|
+
shellSidebarUser,
|
|
10
|
+
shellSidebarWorkspaces,
|
|
11
|
+
} from './data';
|
|
12
|
+
|
|
13
|
+
export interface ShellSidebarPageProps {
|
|
14
|
+
nav: NavGroup[];
|
|
15
|
+
currentHref: string;
|
|
16
|
+
breadcrumbs: BreadcrumbItem[] | null;
|
|
17
|
+
workspaces: Workspace[] | null;
|
|
18
|
+
user: UserMenuUser | null;
|
|
19
|
+
title: ReactNode;
|
|
20
|
+
description: ReactNode;
|
|
21
|
+
emptyTitle: ReactNode;
|
|
22
|
+
emptyDescription: ReactNode;
|
|
23
|
+
emptyActionLabel: string;
|
|
24
|
+
emptySteps: string[];
|
|
25
|
+
/** Primary action of the empty state. */
|
|
26
|
+
onCreate: () => void;
|
|
27
|
+
/** Replaces the empty state with real page content. */
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
/** Other ConsoleShell props (sidebar footer, workspace callbacks, topbar…). */
|
|
30
|
+
shell: Omit<ConsoleShellProps, 'children'>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Console starter: ConsoleShell around an empty Page (title, description, first-run state). */
|
|
34
|
+
export default function ShellSidebarPage({
|
|
35
|
+
nav = shellSidebarNav,
|
|
36
|
+
currentHref = shellSidebarCopy.currentHref,
|
|
37
|
+
breadcrumbs = shellSidebarBreadcrumbs,
|
|
38
|
+
workspaces = shellSidebarWorkspaces,
|
|
39
|
+
user = shellSidebarUser,
|
|
40
|
+
title = shellSidebarCopy.title,
|
|
41
|
+
description = shellSidebarCopy.description,
|
|
42
|
+
emptyTitle = shellSidebarCopy.emptyTitle,
|
|
43
|
+
emptyDescription = shellSidebarCopy.emptyDescription,
|
|
44
|
+
emptyActionLabel = shellSidebarCopy.emptyActionLabel,
|
|
45
|
+
emptySteps = shellSidebarCopy.emptySteps,
|
|
46
|
+
onCreate,
|
|
47
|
+
children,
|
|
48
|
+
shell,
|
|
49
|
+
}: Partial<ShellSidebarPageProps>) {
|
|
50
|
+
return (
|
|
51
|
+
<ConsoleShell
|
|
52
|
+
nav={nav}
|
|
53
|
+
currentHref={currentHref}
|
|
54
|
+
breadcrumbs={breadcrumbs}
|
|
55
|
+
workspaces={workspaces}
|
|
56
|
+
user={user}
|
|
57
|
+
{...shell}
|
|
58
|
+
>
|
|
59
|
+
<Page
|
|
60
|
+
header={
|
|
61
|
+
<PageHeader
|
|
62
|
+
breadcrumbs={null}
|
|
63
|
+
title={title}
|
|
64
|
+
description={description}
|
|
65
|
+
status=""
|
|
66
|
+
meta={[]}
|
|
67
|
+
actions={[]}
|
|
68
|
+
tabs={null}
|
|
69
|
+
/>
|
|
70
|
+
}
|
|
71
|
+
>
|
|
72
|
+
{children ?? (
|
|
73
|
+
<FirstRunEmpty
|
|
74
|
+
titleAs="h2"
|
|
75
|
+
title={emptyTitle}
|
|
76
|
+
description={emptyDescription}
|
|
77
|
+
actionLabel={emptyActionLabel}
|
|
78
|
+
onAction={onCreate}
|
|
79
|
+
steps={emptySteps}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
</Page>
|
|
83
|
+
</ConsoleShell>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { appSidebarNavGroups, appSidebarUser, appSidebarWorkspaces, firstRunSteps } from '@gntik-ai/blocks';
|
|
2
|
+
import type { BreadcrumbItem } from '@gntik-ai/ui';
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
appSidebarNavGroups as shellSidebarNav,
|
|
6
|
+
appSidebarUser as shellSidebarUser,
|
|
7
|
+
appSidebarWorkspaces as shellSidebarWorkspaces,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const shellSidebarBreadcrumbs: BreadcrumbItem[] = [
|
|
11
|
+
{ label: 'Acme Industries', href: '/overview' },
|
|
12
|
+
{ label: 'Projects' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const shellSidebarCopy = {
|
|
16
|
+
currentHref: '/projects',
|
|
17
|
+
title: 'Projects',
|
|
18
|
+
description: 'Services, environments and deployments, grouped by project.',
|
|
19
|
+
emptyTitle: 'Create your first project',
|
|
20
|
+
emptyDescription:
|
|
21
|
+
'Projects group your services, environments and deployments. Start one from a repository in a couple of minutes.',
|
|
22
|
+
emptyActionLabel: 'New project',
|
|
23
|
+
emptySteps: firstRunSteps,
|
|
24
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TemplateMeta } from '../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: TemplateMeta = {
|
|
4
|
+
name: 'Sidebar shell',
|
|
5
|
+
family: 'Shells',
|
|
6
|
+
priority: 'P1',
|
|
7
|
+
status: 'stable',
|
|
8
|
+
description:
|
|
9
|
+
'Console starter: app sidebar, topbar and an empty Page with a title, description and a first-run empty state.',
|
|
10
|
+
layout: 'SidebarLayout',
|
|
11
|
+
blocks: ['app-sidebar', 'app-topbar', 'page-header', 'empty-states'],
|
|
12
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { render, screen, within } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../test/a11y';
|
|
4
|
+
import ShellTopnavPage from './Page';
|
|
5
|
+
|
|
6
|
+
describe('ShellTopnavPage', () => {
|
|
7
|
+
it('renders the navbar, page header and empty body', { timeout: 15000 }, async () => {
|
|
8
|
+
const { container } = render(<ShellTopnavPage />);
|
|
9
|
+
expect(screen.getByRole('main')).toBeInTheDocument();
|
|
10
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Projects' })).toBeInTheDocument();
|
|
11
|
+
const nav = screen.getByRole('navigation', { name: 'Main' });
|
|
12
|
+
expect(within(nav).getByRole('link', { name: 'Projects' })).toHaveAttribute('aria-current', 'page');
|
|
13
|
+
await expectNoAxeViolations(container);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('moves the current link on navigation', { timeout: 15000 }, async () => {
|
|
17
|
+
const onNavigate = vi.fn();
|
|
18
|
+
render(<ShellTopnavPage onNavigate={onNavigate} />);
|
|
19
|
+
const nav = screen.getByRole('navigation', { name: 'Main' });
|
|
20
|
+
await userEvent.click(within(nav).getByRole('link', { name: 'Deployments' }));
|
|
21
|
+
expect(within(nav).getByRole('link', { name: 'Deployments' })).toHaveAttribute('aria-current', 'page');
|
|
22
|
+
expect(within(nav).getByRole('link', { name: 'Projects' })).not.toHaveAttribute('aria-current');
|
|
23
|
+
expect(onNavigate).toHaveBeenCalledWith(expect.objectContaining({ label: 'Deployments' }));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AppTopbar, FirstRunEmpty, PageHeader } from '@gntik-ai/blocks';
|
|
2
|
+
import { Logo, Stack, StackedLayout, type NavItem, type NotificationItem, type UserMenuUser } from '@gntik-ai/ui';
|
|
3
|
+
import { useState, type ReactNode } from 'react';
|
|
4
|
+
import { shellTopnavCopy, shellTopnavLinks, shellTopnavNotifications, shellTopnavUser } from './data';
|
|
5
|
+
|
|
6
|
+
export interface ShellTopnavPageProps {
|
|
7
|
+
/** Brand at the start of the navbar; defaults to the preset Logo with wordmark. */
|
|
8
|
+
brand: ReactNode;
|
|
9
|
+
links: NavItem[];
|
|
10
|
+
/** Initial current link (uncontrolled). */
|
|
11
|
+
defaultHref: string;
|
|
12
|
+
onNavigate: (item: NavItem) => void;
|
|
13
|
+
user: UserMenuUser | null;
|
|
14
|
+
notifications: NotificationItem[] | null;
|
|
15
|
+
onSignOut: () => void;
|
|
16
|
+
title: ReactNode;
|
|
17
|
+
description: ReactNode;
|
|
18
|
+
emptyTitle: ReactNode;
|
|
19
|
+
emptyDescription: ReactNode;
|
|
20
|
+
emptyActionLabel: string;
|
|
21
|
+
emptySteps: string[];
|
|
22
|
+
onCreate: () => void;
|
|
23
|
+
footer: ReactNode;
|
|
24
|
+
/** Replaces the empty state with real page content. */
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Top-navigation starter: StackedLayout with brand, links, AppTopbar actions, a PageHeader and an empty body. */
|
|
29
|
+
export default function ShellTopnavPage({
|
|
30
|
+
brand = <Logo size={24} wordmark />,
|
|
31
|
+
links = shellTopnavLinks,
|
|
32
|
+
defaultHref = shellTopnavCopy.currentHref,
|
|
33
|
+
onNavigate,
|
|
34
|
+
user = shellTopnavUser,
|
|
35
|
+
notifications = shellTopnavNotifications,
|
|
36
|
+
onSignOut,
|
|
37
|
+
title = shellTopnavCopy.title,
|
|
38
|
+
description = shellTopnavCopy.description,
|
|
39
|
+
emptyTitle = shellTopnavCopy.emptyTitle,
|
|
40
|
+
emptyDescription = shellTopnavCopy.emptyDescription,
|
|
41
|
+
emptyActionLabel = shellTopnavCopy.emptyActionLabel,
|
|
42
|
+
emptySteps = shellTopnavCopy.emptySteps,
|
|
43
|
+
onCreate,
|
|
44
|
+
footer = shellTopnavCopy.footer,
|
|
45
|
+
children,
|
|
46
|
+
}: Partial<ShellTopnavPageProps>) {
|
|
47
|
+
const [current, setCurrent] = useState(defaultHref);
|
|
48
|
+
return (
|
|
49
|
+
<StackedLayout
|
|
50
|
+
fullScreen
|
|
51
|
+
brand={brand}
|
|
52
|
+
links={links}
|
|
53
|
+
currentHref={current}
|
|
54
|
+
onNavigate={(item) => {
|
|
55
|
+
if (item.href) setCurrent(item.href);
|
|
56
|
+
onNavigate?.(item);
|
|
57
|
+
}}
|
|
58
|
+
mobileTitle="Navigation"
|
|
59
|
+
actions={
|
|
60
|
+
<AppTopbar
|
|
61
|
+
breadcrumbs={null}
|
|
62
|
+
environment={null}
|
|
63
|
+
notifications={notifications}
|
|
64
|
+
user={user}
|
|
65
|
+
onSignOut={onSignOut}
|
|
66
|
+
/>
|
|
67
|
+
}
|
|
68
|
+
footer={footer}
|
|
69
|
+
>
|
|
70
|
+
{/* PageHeader renders a <header>: inside <main> it is not a second banner landmark. */}
|
|
71
|
+
<Stack gap={6}>
|
|
72
|
+
<PageHeader
|
|
73
|
+
breadcrumbs={null}
|
|
74
|
+
title={title}
|
|
75
|
+
description={description}
|
|
76
|
+
status=""
|
|
77
|
+
meta={[]}
|
|
78
|
+
actions={[]}
|
|
79
|
+
tabs={null}
|
|
80
|
+
/>
|
|
81
|
+
{children ?? (
|
|
82
|
+
<FirstRunEmpty
|
|
83
|
+
titleAs="h2"
|
|
84
|
+
title={emptyTitle}
|
|
85
|
+
description={emptyDescription}
|
|
86
|
+
actionLabel={emptyActionLabel}
|
|
87
|
+
onAction={onCreate}
|
|
88
|
+
steps={emptySteps}
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
</Stack>
|
|
92
|
+
</StackedLayout>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { appSidebarUser, appTopbarNotifications, firstRunSteps } from '@gntik-ai/blocks';
|
|
2
|
+
import { BarChart3, FolderKanban, Home, Rocket, Users } from '@gntik-ai/icons';
|
|
3
|
+
import type { NavItem } from '@gntik-ai/ui';
|
|
4
|
+
|
|
5
|
+
export { appSidebarUser as shellTopnavUser, appTopbarNotifications as shellTopnavNotifications };
|
|
6
|
+
|
|
7
|
+
export const shellTopnavLinks: NavItem[] = [
|
|
8
|
+
{ label: 'Overview', href: '#overview', icon: Home },
|
|
9
|
+
{ label: 'Projects', href: '#projects', icon: FolderKanban },
|
|
10
|
+
{ label: 'Deployments', href: '#deployments', icon: Rocket },
|
|
11
|
+
{ label: 'Analytics', href: '#analytics', icon: BarChart3 },
|
|
12
|
+
{ label: 'Members', href: '#members', icon: Users },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const shellTopnavCopy = {
|
|
16
|
+
currentHref: '#projects',
|
|
17
|
+
title: 'Projects',
|
|
18
|
+
description: 'Services, environments and deployments, grouped by project.',
|
|
19
|
+
emptyTitle: 'Create your first project',
|
|
20
|
+
emptyDescription:
|
|
21
|
+
'Projects group your services, environments and deployments. Start one from a repository in a couple of minutes.',
|
|
22
|
+
emptyActionLabel: 'New project',
|
|
23
|
+
emptySteps: firstRunSteps,
|
|
24
|
+
footer: '© 2026 Acme Industries · Status · Privacy',
|
|
25
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TemplateMeta } from '../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: TemplateMeta = {
|
|
4
|
+
name: 'Top-nav shell',
|
|
5
|
+
family: 'Shells',
|
|
6
|
+
priority: 'P1',
|
|
7
|
+
status: 'stable',
|
|
8
|
+
description:
|
|
9
|
+
'Top-navigation starter: brand, primary links (drawer on small screens), search, notifications, theme and user menu, a page header and an empty body.',
|
|
10
|
+
layout: 'StackedLayout',
|
|
11
|
+
blocks: ['app-topbar', 'page-header', 'empty-states'],
|
|
12
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Check } from '@gntik-ai/icons';
|
|
2
|
+
import { Stack, Text } from '@gntik-ai/ui';
|
|
3
|
+
|
|
4
|
+
export interface BrandPanelCopy {
|
|
5
|
+
eyebrow: string;
|
|
6
|
+
headline: string;
|
|
7
|
+
features: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Brand panel body for AuthLayout `split`: eyebrow, headline and a short feature list (text only). */
|
|
11
|
+
export function BrandPanel({ eyebrow, headline, features }: BrandPanelCopy) {
|
|
12
|
+
return (
|
|
13
|
+
<Stack gap={8}>
|
|
14
|
+
<Stack gap={4}>
|
|
15
|
+
<Text variant="caption" tone="primary" className="font-mono tracking-[0.18em] uppercase">
|
|
16
|
+
{eyebrow}
|
|
17
|
+
</Text>
|
|
18
|
+
<Text variant="display" className="text-[32px] text-balance">
|
|
19
|
+
{headline}
|
|
20
|
+
</Text>
|
|
21
|
+
</Stack>
|
|
22
|
+
<Stack as="ul" gap={3}>
|
|
23
|
+
{features.map((feature) => (
|
|
24
|
+
<Stack as="li" key={feature} direction="row" align="center" gap={3}>
|
|
25
|
+
<Check size={16} aria-hidden className="shrink-0 text-primary-text" />
|
|
26
|
+
<Text variant="supporting" tone="muted">
|
|
27
|
+
{feature}
|
|
28
|
+
</Text>
|
|
29
|
+
</Stack>
|
|
30
|
+
))}
|
|
31
|
+
</Stack>
|
|
32
|
+
</Stack>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../test/a11y';
|
|
4
|
+
import SignInPage from './Page';
|
|
5
|
+
|
|
6
|
+
describe('SignInPage', () => {
|
|
7
|
+
it('renders the form, SSO options and brand panel', async () => {
|
|
8
|
+
const { container } = render(<SignInPage />);
|
|
9
|
+
expect(screen.getByRole('main')).toBeInTheDocument();
|
|
10
|
+
expect(screen.getByRole('complementary')).toBeInTheDocument();
|
|
11
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Sign in to your workspace' })).toBeInTheDocument();
|
|
12
|
+
expect(screen.getByRole('group', { name: 'Single sign-on' })).toBeInTheDocument();
|
|
13
|
+
await expectNoAxeViolations(container);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('validates, then submits the credentials', async () => {
|
|
17
|
+
const onSubmit = vi.fn();
|
|
18
|
+
render(<SignInPage onSubmit={onSubmit} />);
|
|
19
|
+
await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
|
|
20
|
+
expect(screen.getByText('Enter a valid email address.')).toBeInTheDocument();
|
|
21
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
22
|
+
await userEvent.type(screen.getByLabelText('Email'), 'dana@example.com');
|
|
23
|
+
await userEvent.type(screen.getByLabelText('Password'), 'correct horse');
|
|
24
|
+
await userEvent.click(screen.getByRole('button', { name: 'Sign in' }));
|
|
25
|
+
expect(onSubmit).toHaveBeenCalledWith({ email: 'dana@example.com', password: 'correct horse', remember: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('starts single sign-on with the chosen provider', async () => {
|
|
29
|
+
const onSso = vi.fn();
|
|
30
|
+
render(<SignInPage onSso={onSso} />);
|
|
31
|
+
await userEvent.click(screen.getByRole('button', { name: 'Continue with SSO' }));
|
|
32
|
+
expect(onSso).toHaveBeenCalledWith('sso');
|
|
33
|
+
expect(screen.getByRole('button', { name: 'Continue with Google' })).toBeDisabled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('takes the heading copy from props', () => {
|
|
37
|
+
render(<SignInPage title="Welcome back to the console" description="Sign in with your tenant account." />);
|
|
38
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Welcome back to the console' })).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByText('Sign in with your tenant account.')).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { SignInForm, SsoButtons, type SignInValues, type SsoProvider } from '@gntik-ai/blocks';
|
|
2
|
+
import { AuthLayout } from '@gntik-ai/ui';
|
|
3
|
+
import { useState, type ReactNode } from 'react';
|
|
4
|
+
import { BrandPanel, type BrandPanelCopy } from './BrandPanel';
|
|
5
|
+
import { signInBrand, signInSsoProviders } from './data';
|
|
6
|
+
|
|
7
|
+
export interface SignInPageProps {
|
|
8
|
+
/** Heading above the form (default "Sign in to your workspace"). */
|
|
9
|
+
title: ReactNode;
|
|
10
|
+
/** Line under the heading (default "Use your work email to continue."). */
|
|
11
|
+
description: ReactNode;
|
|
12
|
+
/** Small label above the heading. */
|
|
13
|
+
eyebrow: ReactNode;
|
|
14
|
+
/** Signs in. A returned promise shows the loading state; a rejection's message shows in the alert. */
|
|
15
|
+
onSubmit: (values: SignInValues) => void | Promise<void>;
|
|
16
|
+
/** Starts single sign-on with a provider id; the button spins until the page navigates away. */
|
|
17
|
+
onSso: (providerId: string) => void;
|
|
18
|
+
ssoProviders: SsoProvider[];
|
|
19
|
+
error: string | null;
|
|
20
|
+
forgotPasswordHref: string;
|
|
21
|
+
onForgotPassword: () => void;
|
|
22
|
+
signUpHref: string;
|
|
23
|
+
onSignUp: () => void;
|
|
24
|
+
/** Brand panel copy (split layout, large screens). */
|
|
25
|
+
brand: BrandPanelCopy;
|
|
26
|
+
brandFooter: ReactNode;
|
|
27
|
+
logo: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Split sign-in: AuthLayout brand panel + SignInForm with SsoButtons. */
|
|
31
|
+
export default function SignInPage({
|
|
32
|
+
title,
|
|
33
|
+
description,
|
|
34
|
+
eyebrow,
|
|
35
|
+
onSubmit,
|
|
36
|
+
onSso,
|
|
37
|
+
ssoProviders = signInSsoProviders,
|
|
38
|
+
error,
|
|
39
|
+
forgotPasswordHref,
|
|
40
|
+
onForgotPassword,
|
|
41
|
+
signUpHref,
|
|
42
|
+
onSignUp,
|
|
43
|
+
brand = signInBrand,
|
|
44
|
+
brandFooter = signInBrand.footer,
|
|
45
|
+
logo,
|
|
46
|
+
}: Partial<SignInPageProps>) {
|
|
47
|
+
const [ssoLoading, setSsoLoading] = useState<string | null>(null);
|
|
48
|
+
return (
|
|
49
|
+
<AuthLayout fullScreen variant="split" logo={logo} brand={<BrandPanel {...brand} />} brandFooter={brandFooter}>
|
|
50
|
+
<SignInForm
|
|
51
|
+
title={title}
|
|
52
|
+
description={description}
|
|
53
|
+
eyebrow={eyebrow}
|
|
54
|
+
onSubmit={onSubmit}
|
|
55
|
+
error={error}
|
|
56
|
+
forgotPasswordHref={forgotPasswordHref}
|
|
57
|
+
onForgotPassword={onForgotPassword}
|
|
58
|
+
signUpHref={signUpHref}
|
|
59
|
+
onSignUp={onSignUp}
|
|
60
|
+
sso={
|
|
61
|
+
<SsoButtons
|
|
62
|
+
providers={ssoProviders}
|
|
63
|
+
loadingId={ssoLoading}
|
|
64
|
+
onSelect={(id) => {
|
|
65
|
+
setSsoLoading(id);
|
|
66
|
+
onSso?.(id);
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
</AuthLayout>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ssoProviders } from '@gntik-ai/blocks';
|
|
2
|
+
|
|
3
|
+
export { ssoProviders as signInSsoProviders };
|
|
4
|
+
|
|
5
|
+
/** Brand panel copy for the split layout. */
|
|
6
|
+
export const signInBrand = {
|
|
7
|
+
eyebrow: 'Control plane',
|
|
8
|
+
headline: 'Ship with confidence, not guesswork.',
|
|
9
|
+
features: ['Every deployment reviewed and audited', 'Roles and access per project', 'Usage and spend in one place'],
|
|
10
|
+
footer: 'MFA · recovery codes · audited access',
|
|
11
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TemplateMeta } from '../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: TemplateMeta = {
|
|
4
|
+
name: 'Sign in',
|
|
5
|
+
family: 'Auth',
|
|
6
|
+
priority: 'P1',
|
|
7
|
+
status: 'stable',
|
|
8
|
+
description:
|
|
9
|
+
'Split sign-in: brand panel with a short value pitch, email and password form, forgot-password link and single sign-on options.',
|
|
10
|
+
layout: 'AuthLayout',
|
|
11
|
+
blocks: ['sign-in-form', 'sso-buttons'],
|
|
12
|
+
};
|