@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,54 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
// Source is a public contract here: consumers must be able to re-theme every style.
|
|
6
|
+
const namedColours =
|
|
7
|
+
'aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen'
|
|
8
|
+
.split(' ')
|
|
9
|
+
.join('|');
|
|
10
|
+
const forbidden = [
|
|
11
|
+
/[\w:-]+-\[[^\]]+\]/, // Tailwind arbitrary values and variants
|
|
12
|
+
/\bstyle\s*(?:=|:)/,
|
|
13
|
+
/#[\da-f]{3,8}\b/i,
|
|
14
|
+
/\b(?:rgba?|hsla?|hwb|oklch|oklab|lab|lch|color)\s*\(/i,
|
|
15
|
+
/\b\d*\.?\d+(?:px|rem|em)\b/i,
|
|
16
|
+
new RegExp(`(?:['"\x60]|:\\s*)(?:${namedColours})(?:['"\x60]|\\s*[;}])`, 'i'),
|
|
17
|
+
new RegExp(
|
|
18
|
+
`\\b(?:bg|text|border|ring|outline|fill|stroke|shadow|divide|decoration)-(?:${namedColours})(?:-\\d+)?\\b`,
|
|
19
|
+
'i',
|
|
20
|
+
),
|
|
21
|
+
/\bdark:|\bbg-(?:gradient|linear|radial|conic)-|\bglow\b/,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const violations = (source: string) => forbidden.filter((rule) => rule.test(source)).map(String);
|
|
25
|
+
const sources = (directory: string): string[] =>
|
|
26
|
+
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
27
|
+
const path = join(directory, entry.name);
|
|
28
|
+
return entry.isDirectory() ? sources(path) : /\.(?:tsx?|css)$/.test(path) && !/\.test\./.test(path) ? [path] : [];
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('public-hub token-only source contract', () => {
|
|
32
|
+
it.each([
|
|
33
|
+
'className="w-[42px]"',
|
|
34
|
+
'style={{ color: "red" }}',
|
|
35
|
+
// eslint-disable-next-line no-restricted-syntax -- Negative fixture verifies rejection of literal colours.
|
|
36
|
+
'color: #abc;',
|
|
37
|
+
'color: rgb(1 2 3);',
|
|
38
|
+
'color: hsl(1 2% 3%);',
|
|
39
|
+
'color: rebeccapurple;',
|
|
40
|
+
// eslint-disable-next-line no-restricted-syntax -- Negative fixture verifies rejection of palette classes.
|
|
41
|
+
'className="text-white"',
|
|
42
|
+
'width: 2px;',
|
|
43
|
+
'padding: 1rem;',
|
|
44
|
+
'border-radius: .5em;',
|
|
45
|
+
])('rejects prohibited styling: %s', (source) => {
|
|
46
|
+
expect(violations(source).length).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('uses only kit classes and spacing scales across every template source and example', () => {
|
|
50
|
+
const files = sources(join(process.cwd(), 'src/public-hub'));
|
|
51
|
+
expect(files.length).toBeGreaterThan(3);
|
|
52
|
+
for (const file of files) expect(violations(readFileSync(file, 'utf8')), file).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Product-agnostic English copy; optional content is supplied explicitly. */
|
|
2
|
+
export const publicHubCopy = {
|
|
3
|
+
title: 'Welcome',
|
|
4
|
+
primaryAction: { label: 'Sign in', href: '#sign-in' },
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const publicHubFixture = {
|
|
8
|
+
...publicHubCopy,
|
|
9
|
+
eyebrow: 'Start here',
|
|
10
|
+
description: 'Choose how to continue to your workspace.',
|
|
11
|
+
cards: [
|
|
12
|
+
{
|
|
13
|
+
title: 'Your workspace',
|
|
14
|
+
body: 'Keep your projects and resources together.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: 'Your team',
|
|
18
|
+
body: 'Share your work and collaborate with others.',
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
secondaryAction: { label: 'Create an account', href: '#sign-up' },
|
|
22
|
+
tertiaryLink: { label: 'Recover access', href: '#recovery' },
|
|
23
|
+
footer: 'Need help? Contact your account administrator.',
|
|
24
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ThemeProvider } from '@gntik-ai/ui';
|
|
2
|
+
import PublicHubPage from '../Page';
|
|
3
|
+
import { publicHubFixture } from '../data';
|
|
4
|
+
|
|
5
|
+
export default function Dark() {
|
|
6
|
+
return (
|
|
7
|
+
<ThemeProvider defaultMode="dark" storageKey={null}>
|
|
8
|
+
<PublicHubPage {...publicHubFixture} />
|
|
9
|
+
</ThemeProvider>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ThemeProvider } from '@gntik-ai/ui';
|
|
2
|
+
import PublicHubPage from '../Page';
|
|
3
|
+
import { publicHubFixture } from '../data';
|
|
4
|
+
|
|
5
|
+
export default function HighContrast() {
|
|
6
|
+
return (
|
|
7
|
+
<ThemeProvider defaultMode="high_contrast" storageKey={null}>
|
|
8
|
+
<PublicHubPage {...publicHubFixture} />
|
|
9
|
+
</ThemeProvider>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ThemeProvider } from '@gntik-ai/ui';
|
|
2
|
+
import PublicHubPage from '../Page';
|
|
3
|
+
import { publicHubFixture } from '../data';
|
|
4
|
+
|
|
5
|
+
export default function Light() {
|
|
6
|
+
return (
|
|
7
|
+
<ThemeProvider defaultMode="light" storageKey={null}>
|
|
8
|
+
<PublicHubPage {...publicHubFixture} />
|
|
9
|
+
</ThemeProvider>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { TemplateMeta } from '../meta';
|
|
2
|
+
|
|
3
|
+
export const meta = {
|
|
4
|
+
name: 'Public hub',
|
|
5
|
+
family: 'Auth',
|
|
6
|
+
priority: 'P1',
|
|
7
|
+
status: 'stable',
|
|
8
|
+
description: `Unauthenticated entry hub on AuthLayout full-bleed with PageHeader, explanatory cards and navigation actions. Use public-hub instead of landing for an account entry point without a marketing hero or screenshot; use sign-in-card for a single sign-in form. Recommend 2 to 4 cards; any number is supported. Cards use the kit sm breakpoint: one column below sm, two from sm. ARIA: a section labelled by the text span inside the single PageHeader h1, h2 card headings and a ul/li card list. The title span owns the aria-labelledby target id in server HTML and after hydration; PageHeader has no heading-id prop. Loading replaces content with decorative kit Skeletons in an aria-busy section named by loadingLabel. Keyboard: Tab and Shift+Tab move through the primary, secondary and tertiary links in visual order; Enter follows href and invokes onClick, with visible focus and no trap. AuthLayout retains its skip link, named "Skip to main content", before the content in both states. The PageHeader title inherits truncation for long titles.
|
|
9
|
+
|
|
10
|
+
## Props
|
|
11
|
+
|
|
12
|
+
| Prop | Type | Default / behaviour |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| eyebrow | string | Omitted; optional Badge before PageHeader |
|
|
15
|
+
| title | string | Welcome; PageHeader h1 |
|
|
16
|
+
| description | string | Omitted; PageHeader description |
|
|
17
|
+
| cards | { title: string; body: string }[] | Omitted; explanatory list with h2 headings |
|
|
18
|
+
| primaryAction | { label: string; href: string; onClick?: MouseEventHandler<HTMLAnchorElement> } | Sign in, #sign-in; Button rendered as an anchor |
|
|
19
|
+
| secondaryAction | Same action shape | Omitted; secondary Button rendered as an anchor |
|
|
20
|
+
| tertiaryLink | Same action shape | Omitted; one kit Link |
|
|
21
|
+
| footer | string | Omitted; Separator then supporting Text |
|
|
22
|
+
| loading | boolean | false; replaces header, cards and actions; hides eyebrow/footer copy |
|
|
23
|
+
| logo | ReactNode | Active preset logo from AuthLayout |
|
|
24
|
+
| loadingLabel | string | Loading; accessible name of the busy section |
|
|
25
|
+
|
|
26
|
+
Optional content has no default fixtures and creates no empty wrappers; an empty cards array also renders no list. Without footer there is no separator. Preview fixtures: public-hub--full, public-hub--minimal, public-hub--loading and public-hub--minimal-loading.
|
|
27
|
+
|
|
28
|
+
## Theme examples
|
|
29
|
+
|
|
30
|
+
Import ThemeProvider from @gntik-ai/ui and PublicHubPage from @gntik-ai/templates. Pass account routes and copy as props. Each theme supports the same full, minimal and loading composition.
|
|
31
|
+
|
|
32
|
+
\`\`\`tsx
|
|
33
|
+
<ThemeProvider defaultMode="dark" storageKey={null}>
|
|
34
|
+
<PublicHubPage title="Welcome" primaryAction={{ label: 'Sign in', href: '/sign-in' }} />
|
|
35
|
+
</ThemeProvider>
|
|
36
|
+
\`\`\`
|
|
37
|
+
|
|
38
|
+
\`\`\`tsx
|
|
39
|
+
<ThemeProvider defaultMode="light" storageKey={null}>
|
|
40
|
+
<PublicHubPage title="Welcome" primaryAction={{ label: 'Sign in', href: '/sign-in' }} />
|
|
41
|
+
</ThemeProvider>
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
\`\`\`tsx
|
|
45
|
+
<ThemeProvider defaultMode="high_contrast" storageKey={null}>
|
|
46
|
+
<PublicHubPage title="Welcome" primaryAction={{ label: 'Sign in', href: '/sign-in' }} />
|
|
47
|
+
</ThemeProvider>
|
|
48
|
+
\`\`\``,
|
|
49
|
+
layout: 'AuthLayout',
|
|
50
|
+
blocks: ['page-header'],
|
|
51
|
+
pattern:
|
|
52
|
+
'Named section (aria-labelledby points to the title span inside the PageHeader h1, including in server HTML); h2 card headings in a ul/li list. Loading: aria-busy=true and aria-label=loadingLabel, with decorative Skeletons and no content links or headings.',
|
|
53
|
+
keyboard: [
|
|
54
|
+
[
|
|
55
|
+
'Tab / Shift+Tab',
|
|
56
|
+
'Move through primary, secondary and tertiary navigation links in reading order, after the AuthLayout skip link named "Skip to main content"; focus is visible and can leave the page.',
|
|
57
|
+
],
|
|
58
|
+
['Enter', 'Follow the focused anchor href and invoke its optional onClick handler.'],
|
|
59
|
+
],
|
|
60
|
+
} satisfies TemplateMeta & { pattern: string; keyboard: [string, string][] };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { expectNoAxeViolations } from '../test/a11y';
|
|
4
|
+
import ResetPasswordPage from './Page';
|
|
5
|
+
|
|
6
|
+
describe('ResetPasswordPage', () => {
|
|
7
|
+
it('renders the new-password form', async () => {
|
|
8
|
+
const { container } = render(<ResetPasswordPage />);
|
|
9
|
+
expect(screen.getByRole('main')).toBeInTheDocument();
|
|
10
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Set a new password' })).toBeInTheDocument();
|
|
11
|
+
await expectNoAxeViolations(container);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('saves the password and continues from the success state', async () => {
|
|
15
|
+
const onSubmit = vi.fn();
|
|
16
|
+
const onContinue = vi.fn();
|
|
17
|
+
const { container } = render(<ResetPasswordPage onSubmit={onSubmit} onContinue={onContinue} />);
|
|
18
|
+
await userEvent.type(screen.getByLabelText('New password'), 'a-much-longer-pass');
|
|
19
|
+
await userEvent.type(screen.getByLabelText('Confirm password'), 'a-much-longer-pass');
|
|
20
|
+
await userEvent.click(screen.getByRole('button', { name: 'Update password' }));
|
|
21
|
+
expect(onSubmit).toHaveBeenCalledWith('a-much-longer-pass');
|
|
22
|
+
expect(await screen.findByRole('heading', { level: 1, name: 'Password updated' })).toBeInTheDocument();
|
|
23
|
+
await userEvent.click(screen.getByRole('button', { name: 'Continue to sign in' }));
|
|
24
|
+
expect(onContinue).toHaveBeenCalledOnce();
|
|
25
|
+
await expectNoAxeViolations(container);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ResetPasswordForm } from '@gntik-ai/blocks';
|
|
2
|
+
import { AuthLayout, Link } from '@gntik-ai/ui';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { resetPasswordSample } from './data';
|
|
5
|
+
|
|
6
|
+
export interface ResetPasswordPageProps {
|
|
7
|
+
/** Saves the new password. A returned promise shows the loading state; a rejection shows in the alert. */
|
|
8
|
+
onSubmit: (password: string) => void | Promise<void>;
|
|
9
|
+
/** "Continue to sign in" from the success state. */
|
|
10
|
+
onContinue: () => void;
|
|
11
|
+
error: string | null;
|
|
12
|
+
minPasswordLength: number;
|
|
13
|
+
signInHref: string;
|
|
14
|
+
logo: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Card new-password step: AuthLayout `card` + ResetPasswordForm (form → success). */
|
|
18
|
+
export default function ResetPasswordPage({
|
|
19
|
+
onSubmit,
|
|
20
|
+
onContinue,
|
|
21
|
+
error,
|
|
22
|
+
minPasswordLength = resetPasswordSample.minPasswordLength,
|
|
23
|
+
signInHref = resetPasswordSample.signInHref,
|
|
24
|
+
logo,
|
|
25
|
+
}: Partial<ResetPasswordPageProps>) {
|
|
26
|
+
return (
|
|
27
|
+
<AuthLayout
|
|
28
|
+
fullScreen
|
|
29
|
+
variant="card"
|
|
30
|
+
logo={logo}
|
|
31
|
+
skipLinkLabel="Skip to password form"
|
|
32
|
+
footer={
|
|
33
|
+
<>
|
|
34
|
+
Remembered it? <Link href={signInHref}>Back to sign in</Link>
|
|
35
|
+
</>
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
<ResetPasswordForm
|
|
39
|
+
onSubmit={onSubmit}
|
|
40
|
+
onContinue={onContinue}
|
|
41
|
+
error={error}
|
|
42
|
+
minPasswordLength={minPasswordLength}
|
|
43
|
+
/>
|
|
44
|
+
</AuthLayout>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TemplateMeta } from '../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: TemplateMeta = {
|
|
4
|
+
name: 'Reset password',
|
|
5
|
+
family: 'Auth',
|
|
6
|
+
priority: 'P1',
|
|
7
|
+
status: 'stable',
|
|
8
|
+
description:
|
|
9
|
+
'Centred card new-password step: password with a strength meter and confirmation, then a success state that continues to sign in.',
|
|
10
|
+
layout: 'AuthLayout',
|
|
11
|
+
blocks: ['reset-password-form'],
|
|
12
|
+
};
|
|
@@ -0,0 +1,260 @@
|
|
|
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 { DEPLOYMENT_DETAILS, DEPLOYMENT_EVENTS } from '@gntik-ai/blocks';
|
|
5
|
+
import ResourceDetailPage from './Page';
|
|
6
|
+
import { detailActivity, detailDangerActions } from './data';
|
|
7
|
+
|
|
8
|
+
describe('ResourceDetailPage', () => {
|
|
9
|
+
it('omits read-only history even when editable events are supplied', { timeout: 15000 }, async () => {
|
|
10
|
+
const { container } = render(<ResourceDetailPage readOnly name="Resource record" events={DEPLOYMENT_EVENTS} />);
|
|
11
|
+
expect(screen.queryByRole('heading', { name: 'Status history' })).not.toBeInTheDocument();
|
|
12
|
+
expect(screen.queryByRole('list', { name: 'Status history' })).not.toBeInTheDocument();
|
|
13
|
+
expect(screen.queryByRole('region', { name: 'Status history' })).not.toBeInTheDocument();
|
|
14
|
+
expect(within(screen.getByRole('main')).queryByRole('definition')).not.toBeInTheDocument();
|
|
15
|
+
expect(container.querySelector('time')).toBeNull();
|
|
16
|
+
await expectNoAxeViolations(container);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('honors explicit read-only header props and the Edit handler', { timeout: 15000 }, async () => {
|
|
20
|
+
const onAction = vi.fn();
|
|
21
|
+
const onEditDetails = vi.fn();
|
|
22
|
+
const { container } = render(
|
|
23
|
+
<ResourceDetailPage
|
|
24
|
+
readOnly
|
|
25
|
+
name="Resource record"
|
|
26
|
+
description="An explicit description."
|
|
27
|
+
overviewDescription="Explicit detail copy."
|
|
28
|
+
meta={[{ label: 'Identifier', value: 'record_42' }]}
|
|
29
|
+
actions={[{ label: 'Inspect resource', onClick: onAction }]}
|
|
30
|
+
onEditDetails={onEditDetails}
|
|
31
|
+
/>,
|
|
32
|
+
);
|
|
33
|
+
expect(screen.getByText('An explicit description.')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('Explicit detail copy.')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('record_42')).toBeInTheDocument();
|
|
36
|
+
// Both responsive header action copies are visible to jsdom without layout CSS.
|
|
37
|
+
await userEvent.click(screen.getAllByRole('button', { name: 'Inspect resource' })[0]!);
|
|
38
|
+
await userEvent.click(screen.getByRole('button', { name: 'Edit' }));
|
|
39
|
+
expect(onAction).toHaveBeenCalledOnce();
|
|
40
|
+
expect(onEditDetails).toHaveBeenCalledOnce();
|
|
41
|
+
await expectNoAxeViolations(container);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('ignores read-only history in the editable default', { timeout: 15000 }, async () => {
|
|
45
|
+
const { container } = render(<ResourceDetailPage history={[]} />);
|
|
46
|
+
expect(screen.getAllByRole('tab')).toHaveLength(3);
|
|
47
|
+
expect(screen.getByRole('list', { name: 'Status history' })).toBeInTheDocument();
|
|
48
|
+
expect(screen.queryByRole('heading', { name: 'No status history' })).not.toBeInTheDocument();
|
|
49
|
+
await expectNoAxeViolations(container);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('models table detail without a history card or editable tabs', { timeout: 15000 }, async () => {
|
|
53
|
+
const { default: TableDetail } = await import('./examples/postgres-table-detail');
|
|
54
|
+
const { container } = render(<TableDetail />);
|
|
55
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Table detail' })).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText('id, title, created_at')).toBeInTheDocument();
|
|
57
|
+
expect(screen.queryByRole('heading', { name: 'Status history' })).not.toBeInTheDocument();
|
|
58
|
+
expect(screen.queryByRole('list', { name: 'Status history' })).not.toBeInTheDocument();
|
|
59
|
+
expect(screen.queryByRole('region', { name: 'Status history' })).not.toBeInTheDocument();
|
|
60
|
+
expect(screen.getAllByRole('tab')).toHaveLength(1);
|
|
61
|
+
await expectNoAxeViolations(container);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('models realtime configuration without a history card or editable tabs', { timeout: 15000 }, async () => {
|
|
65
|
+
const { default: RealtimeConfiguration } = await import('./examples/realtime-configuration');
|
|
66
|
+
const { container } = render(<RealtimeConfiguration />);
|
|
67
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Realtime configuration' })).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByText('workspace_42')).toBeInTheDocument();
|
|
69
|
+
expect(screen.queryByRole('heading', { name: 'Status history' })).not.toBeInTheDocument();
|
|
70
|
+
expect(screen.queryByRole('list', { name: 'Status history' })).not.toBeInTheDocument();
|
|
71
|
+
expect(screen.queryByRole('region', { name: 'Status history' })).not.toBeInTheDocument();
|
|
72
|
+
expect(screen.getAllByRole('tab')).toHaveLength(1);
|
|
73
|
+
await expectNoAxeViolations(container);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('models a runtime without a transition as explicit empty history', { timeout: 15000 }, async () => {
|
|
77
|
+
const { default: RuntimeWithoutTransition } = await import('./examples/runtime-without-transition');
|
|
78
|
+
const { container } = render(<RuntimeWithoutTransition />);
|
|
79
|
+
expect(screen.getByRole('region', { name: 'Status history' })).toHaveTextContent('No status history');
|
|
80
|
+
expect(within(screen.getByRole('main')).queryByRole('listitem')).not.toBeInTheDocument();
|
|
81
|
+
expect(container.querySelector('time')).toBeNull();
|
|
82
|
+
await expectNoAxeViolations(container);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('models a runtime transition with one event and no invented actor', { timeout: 15000 }, async () => {
|
|
86
|
+
const { default: RuntimeWithTransition } = await import('./examples/runtime-with-transition');
|
|
87
|
+
const { container } = render(<RuntimeWithTransition />);
|
|
88
|
+
const list = screen.getByRole('list', { name: 'Status history' });
|
|
89
|
+
expect(within(list).getAllByRole('listitem')).toHaveLength(1);
|
|
90
|
+
expect(within(list).getByText('Ready')).toBeInTheDocument();
|
|
91
|
+
expect(list.querySelectorAll('time')).toHaveLength(1);
|
|
92
|
+
expect(list.querySelector('time')).toHaveAttribute('datetime', '2026-05-01T10:00:00.000Z');
|
|
93
|
+
expect(list).not.toHaveTextContent('by ');
|
|
94
|
+
await expectNoAxeViolations(container);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('shows every supplied read-only event without collapsing or adding actors', { timeout: 15000 }, async () => {
|
|
98
|
+
const history = [
|
|
99
|
+
{ id: '1', status: 'Ready', tone: 'success' as const, title: 'Runtime ready', at: '2026-05-01T10:00:00Z' },
|
|
100
|
+
{ id: '2', status: 'Starting', tone: 'info' as const, title: 'Runtime starting', at: '2026-05-01T09:00:00Z' },
|
|
101
|
+
{
|
|
102
|
+
id: '3',
|
|
103
|
+
status: 'Scheduled',
|
|
104
|
+
tone: 'neutral' as const,
|
|
105
|
+
title: 'Runtime scheduled',
|
|
106
|
+
at: '2026-05-01T08:00:00Z',
|
|
107
|
+
},
|
|
108
|
+
{ id: '4', status: 'Pending', tone: 'warning' as const, title: 'Runtime pending', at: '2026-05-01T07:00:00Z' },
|
|
109
|
+
{
|
|
110
|
+
id: '5',
|
|
111
|
+
status: 'Requested',
|
|
112
|
+
tone: 'neutral' as const,
|
|
113
|
+
title: 'Runtime requested',
|
|
114
|
+
at: '2026-05-01T06:00:00Z',
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
const { container } = render(<ResourceDetailPage readOnly name="Function runtime" history={history} />);
|
|
118
|
+
const list = screen.getByRole('list', { name: 'Status history' });
|
|
119
|
+
expect(within(list).getAllByRole('listitem')).toHaveLength(5);
|
|
120
|
+
expect(list.querySelectorAll('time')).toHaveLength(5);
|
|
121
|
+
expect(list).not.toHaveTextContent('by ');
|
|
122
|
+
expect(screen.queryByRole('button', { name: /earlier/ })).not.toBeInTheDocument();
|
|
123
|
+
await expectNoAxeViolations(container);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('renders the named empty state for explicit empty read-only history', { timeout: 15000 }, async () => {
|
|
127
|
+
const { container } = render(<ResourceDetailPage readOnly name="Function runtime" history={[]} />);
|
|
128
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Status history' })).toBeInTheDocument();
|
|
129
|
+
const emptyTitle = screen.getByRole('heading', { name: 'No status history' });
|
|
130
|
+
expect(emptyTitle.closest('[role="region"]')).toHaveAccessibleName('Status history');
|
|
131
|
+
expect(screen.queryByRole('list', { name: 'Status history' })).not.toBeInTheDocument();
|
|
132
|
+
expect(within(screen.getByRole('main')).queryByRole('listitem')).not.toBeInTheDocument();
|
|
133
|
+
expect(container.querySelector('time')).toBeNull();
|
|
134
|
+
for (const event of DEPLOYMENT_EVENTS) {
|
|
135
|
+
expect(container).not.toHaveTextContent(event.title);
|
|
136
|
+
}
|
|
137
|
+
await expectNoAxeViolations(container);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('uses only explicit resource data in read-only mode', { timeout: 15000 }, async () => {
|
|
141
|
+
const { container } = render(
|
|
142
|
+
<ResourceDetailPage
|
|
143
|
+
readOnly
|
|
144
|
+
name="Resource record"
|
|
145
|
+
status="active"
|
|
146
|
+
details={[{ id: 'identifier', label: 'Identifier', value: 'record_42' }]}
|
|
147
|
+
/>,
|
|
148
|
+
);
|
|
149
|
+
const main = screen.getByRole('main');
|
|
150
|
+
expect(within(main).getByRole('heading', { level: 1, name: 'Resource record' })).toBeInTheDocument();
|
|
151
|
+
expect(within(main).getByText('active')).toBeInTheDocument();
|
|
152
|
+
expect(within(main).getByText('record_42')).toBeInTheDocument();
|
|
153
|
+
const header = main.querySelector('header');
|
|
154
|
+
expect(header).not.toBeNull();
|
|
155
|
+
expect(header?.querySelector('dl')).toBeNull();
|
|
156
|
+
expect(header?.querySelector('button')).toBeNull();
|
|
157
|
+
expect(screen.queryByRole('button', { name: 'Edit' })).not.toBeInTheDocument();
|
|
158
|
+
for (const text of [
|
|
159
|
+
'orders-api',
|
|
160
|
+
'eu-west-1',
|
|
161
|
+
'View logs',
|
|
162
|
+
'Redeploy',
|
|
163
|
+
'Settings of this deployment.',
|
|
164
|
+
'Settings of this resource.',
|
|
165
|
+
'Public REST API for orders, carts and checkout.',
|
|
166
|
+
]) {
|
|
167
|
+
expect(container).not.toHaveTextContent(text);
|
|
168
|
+
}
|
|
169
|
+
for (const item of DEPLOYMENT_DETAILS) {
|
|
170
|
+
if (item.value) expect(container).not.toHaveTextContent(item.value);
|
|
171
|
+
}
|
|
172
|
+
for (const event of DEPLOYMENT_EVENTS) {
|
|
173
|
+
for (const text of [event.status, event.title, event.description, event.actor]) {
|
|
174
|
+
if (text) expect(main).not.toHaveTextContent(text);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
await expectNoAxeViolations(container);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('keeps only Overview in read-only mode and ignores editable inputs', { timeout: 15000 }, async () => {
|
|
181
|
+
const onTabChange = vi.fn();
|
|
182
|
+
const onSaveSettings = vi.fn();
|
|
183
|
+
const { container } = render(
|
|
184
|
+
<ResourceDetailPage
|
|
185
|
+
readOnly
|
|
186
|
+
name="Function runtime"
|
|
187
|
+
activity={detailActivity}
|
|
188
|
+
dangerActions={detailDangerActions}
|
|
189
|
+
onSaveSettings={onSaveSettings}
|
|
190
|
+
defaultTab="settings"
|
|
191
|
+
onTabChange={onTabChange}
|
|
192
|
+
/>,
|
|
193
|
+
);
|
|
194
|
+
const tablist = screen.getByRole('tablist', { name: 'Resource sections' });
|
|
195
|
+
expect(within(tablist).getAllByRole('tab')).toHaveLength(1);
|
|
196
|
+
const overview = within(tablist).getByRole('tab', { name: 'Overview' });
|
|
197
|
+
expect(overview).toHaveAttribute('aria-selected', 'true');
|
|
198
|
+
expect(screen.queryByRole('tab', { name: 'Activity' })).not.toBeInTheDocument();
|
|
199
|
+
expect(screen.queryByRole('tab', { name: 'Settings' })).not.toBeInTheDocument();
|
|
200
|
+
expect(screen.queryByRole('button', { name: 'Save changes' })).not.toBeInTheDocument();
|
|
201
|
+
expect(screen.queryByRole('heading', { name: 'Danger zone' })).not.toBeInTheDocument();
|
|
202
|
+
expect(screen.queryByRole('region', { name: 'Activity, newest first' })).not.toBeInTheDocument();
|
|
203
|
+
screen.getByRole('main').focus();
|
|
204
|
+
await userEvent.tab();
|
|
205
|
+
expect(overview).toHaveFocus();
|
|
206
|
+
await userEvent.keyboard('{ArrowRight}{Enter}{ArrowLeft}{Enter}{Home}{End}');
|
|
207
|
+
expect(overview).toHaveFocus();
|
|
208
|
+
expect(onTabChange).not.toHaveBeenCalledWith('activity');
|
|
209
|
+
expect(onTabChange).not.toHaveBeenCalledWith('settings');
|
|
210
|
+
expect(onSaveSettings).not.toHaveBeenCalled();
|
|
211
|
+
await expectNoAxeViolations(container);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('renders the header with status and the overview tab', { timeout: 15000 }, async () => {
|
|
215
|
+
const { container } = render(<ResourceDetailPage />);
|
|
216
|
+
expect(screen.getByRole('main')).toBeInTheDocument();
|
|
217
|
+
expect(screen.getByRole('heading', { level: 1, name: 'orders-api' })).toBeInTheDocument();
|
|
218
|
+
expect(screen.getByRole('tab', { name: 'Overview' })).toHaveAttribute('aria-selected', 'true');
|
|
219
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Configuration' })).toBeInTheDocument();
|
|
220
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Status history' })).toBeInTheDocument();
|
|
221
|
+
expect(screen.getByRole('list', { name: 'Status history' })).toBeInTheDocument();
|
|
222
|
+
await expectNoAxeViolations(container);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('switches to Activity with the keyboard', { timeout: 15000 }, async () => {
|
|
226
|
+
const onTabChange = vi.fn();
|
|
227
|
+
render(<ResourceDetailPage onTabChange={onTabChange} />);
|
|
228
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Overview' }));
|
|
229
|
+
await userEvent.keyboard('{ArrowRight}{Enter}');
|
|
230
|
+
expect(onTabChange).toHaveBeenCalledWith('activity');
|
|
231
|
+
expect(screen.getByRole('region', { name: 'Activity, newest first' })).toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('saves the settings form and shows the danger zone', { timeout: 15000 }, async () => {
|
|
235
|
+
const onSaveSettings = vi.fn();
|
|
236
|
+
const { container } = render(<ResourceDetailPage defaultTab="settings" onSaveSettings={onSaveSettings} />);
|
|
237
|
+
const save = screen.getByRole('button', { name: 'Save changes' });
|
|
238
|
+
expect(save).toBeDisabled();
|
|
239
|
+
const nameInput = screen.getByRole('textbox', { name: /Name/ });
|
|
240
|
+
await userEvent.clear(nameInput);
|
|
241
|
+
await userEvent.type(nameInput, 'orders-api-v2');
|
|
242
|
+
await userEvent.click(save);
|
|
243
|
+
expect(onSaveSettings).toHaveBeenCalledWith(expect.objectContaining({ name: 'orders-api-v2' }));
|
|
244
|
+
expect(screen.getByRole('heading', { name: 'Danger zone' })).toBeInTheDocument();
|
|
245
|
+
await expectNoAxeViolations(container);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('takes the Overview card copy from props', { timeout: 15000 }, () => {
|
|
249
|
+
render(
|
|
250
|
+
<ResourceDetailPage
|
|
251
|
+
overviewTitle="Function settings"
|
|
252
|
+
overviewDescription="Runtime and limits."
|
|
253
|
+
historyTitle="Deploy history"
|
|
254
|
+
/>,
|
|
255
|
+
);
|
|
256
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Function settings' })).toBeInTheDocument();
|
|
257
|
+
expect(screen.getByText('Runtime and limits.')).toBeInTheDocument();
|
|
258
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Deploy history' })).toBeInTheDocument();
|
|
259
|
+
});
|
|
260
|
+
});
|