@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,174 @@
|
|
|
1
|
+
import { PageHeader } from '@gntik-ai/blocks';
|
|
2
|
+
import { ArrowRight, Rss } from '@gntik-ai/icons';
|
|
3
|
+
import {
|
|
4
|
+
Badge,
|
|
5
|
+
Button,
|
|
6
|
+
EmptyState,
|
|
7
|
+
Heading,
|
|
8
|
+
HStack,
|
|
9
|
+
Link,
|
|
10
|
+
Page,
|
|
11
|
+
Stack,
|
|
12
|
+
Text,
|
|
13
|
+
Timestamp,
|
|
14
|
+
Toggle,
|
|
15
|
+
ToggleGroup,
|
|
16
|
+
type BreadcrumbItem,
|
|
17
|
+
} from '@gntik-ai/ui';
|
|
18
|
+
import { useId, useState } from 'react';
|
|
19
|
+
import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
|
|
20
|
+
import {
|
|
21
|
+
changelogBreadcrumbs,
|
|
22
|
+
changelogEntries,
|
|
23
|
+
changelogTagLabels,
|
|
24
|
+
changelogTagTones,
|
|
25
|
+
type ChangelogEntry,
|
|
26
|
+
type ChangelogTag,
|
|
27
|
+
} from './data';
|
|
28
|
+
|
|
29
|
+
export interface ChangelogProps {
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
/** Entries, newest first. */
|
|
33
|
+
entries: ChangelogEntry[];
|
|
34
|
+
/** Tags initially pressed; none = every entry. */
|
|
35
|
+
defaultTags: ChangelogTag[];
|
|
36
|
+
onTagsChange: (tags: ChangelogTag[]) => void;
|
|
37
|
+
/** Subscribe action (RSS, email…); omit to hide it. */
|
|
38
|
+
onSubscribe: (() => void) | null;
|
|
39
|
+
breadcrumbs: BreadcrumbItem[];
|
|
40
|
+
currentHref: string;
|
|
41
|
+
shell: Omit<ConsoleShellProps, 'children'>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const isTag = (v: string): v is ChangelogTag => v in changelogTagLabels;
|
|
45
|
+
|
|
46
|
+
/** Changelog: dated release entries with tag badges and a "New" badge, filtered by tag. */
|
|
47
|
+
export default function ChangelogPage(props: Partial<ChangelogProps>) {
|
|
48
|
+
const {
|
|
49
|
+
title = 'Changelog',
|
|
50
|
+
description = 'New features, improvements and fixes, newest first.',
|
|
51
|
+
entries = changelogEntries,
|
|
52
|
+
defaultTags = [],
|
|
53
|
+
onTagsChange,
|
|
54
|
+
onSubscribe = () => {},
|
|
55
|
+
breadcrumbs = changelogBreadcrumbs,
|
|
56
|
+
currentHref = '/help/changelog',
|
|
57
|
+
shell,
|
|
58
|
+
} = props;
|
|
59
|
+
const [tags, setTags] = useState<ChangelogTag[]>(defaultTags);
|
|
60
|
+
const countId = useId();
|
|
61
|
+
const tagsInUse = (Object.keys(changelogTagLabels) as ChangelogTag[]).filter((t) =>
|
|
62
|
+
entries.some((e) => e.tags.includes(t)),
|
|
63
|
+
);
|
|
64
|
+
const visible = tags.length === 0 ? entries : entries.filter((e) => e.tags.some((t) => tags.includes(t)));
|
|
65
|
+
|
|
66
|
+
const changeTags = (next: string[]) => {
|
|
67
|
+
const valid = next.filter(isTag);
|
|
68
|
+
setTags(valid);
|
|
69
|
+
onTagsChange?.(valid);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<ConsoleShell breadcrumbs={breadcrumbs} currentHref={currentHref} {...shell}>
|
|
74
|
+
<Page
|
|
75
|
+
header={
|
|
76
|
+
<PageHeader
|
|
77
|
+
breadcrumbs={null}
|
|
78
|
+
title={title}
|
|
79
|
+
description={description}
|
|
80
|
+
status=""
|
|
81
|
+
meta={[]}
|
|
82
|
+
tabs={null}
|
|
83
|
+
actions={onSubscribe ? [{ label: 'Subscribe', icon: Rss, variant: 'secondary', onClick: onSubscribe }] : []}
|
|
84
|
+
/>
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
<Stack gap={8}>
|
|
88
|
+
<HStack gap={3} wrap>
|
|
89
|
+
<ToggleGroup
|
|
90
|
+
aria-label="Filter by tag"
|
|
91
|
+
aria-describedby={countId}
|
|
92
|
+
size="sm"
|
|
93
|
+
multiple
|
|
94
|
+
value={tags}
|
|
95
|
+
onValueChange={changeTags}
|
|
96
|
+
>
|
|
97
|
+
{tagsInUse.map((t) => (
|
|
98
|
+
<Toggle key={t} value={t}>
|
|
99
|
+
{changelogTagLabels[t]}
|
|
100
|
+
</Toggle>
|
|
101
|
+
))}
|
|
102
|
+
</ToggleGroup>
|
|
103
|
+
{tags.length > 0 && (
|
|
104
|
+
<Button variant="ghost" size="sm" onClick={() => changeTags([])}>
|
|
105
|
+
Show all
|
|
106
|
+
</Button>
|
|
107
|
+
)}
|
|
108
|
+
<Text id={countId} as="span" variant="caption" tone="muted" aria-live="polite">
|
|
109
|
+
{visible.length === 1 ? '1 release' : `${visible.length} releases`}
|
|
110
|
+
</Text>
|
|
111
|
+
</HStack>
|
|
112
|
+
{visible.length === 0 ? (
|
|
113
|
+
<EmptyState
|
|
114
|
+
size="sm"
|
|
115
|
+
titleAs="h2"
|
|
116
|
+
title="No releases with these tags"
|
|
117
|
+
description="Pick another tag or show every release."
|
|
118
|
+
/>
|
|
119
|
+
) : (
|
|
120
|
+
<ol aria-label="Releases" className="flex flex-col divide-y divide-border">
|
|
121
|
+
{visible.map((e) => (
|
|
122
|
+
<li key={e.id} className="grid gap-3 py-7 first:pt-0 md:grid-cols-[10rem_1fr] md:gap-8">
|
|
123
|
+
<Stack gap={1}>
|
|
124
|
+
<Timestamp
|
|
125
|
+
value={e.date}
|
|
126
|
+
format="absolute"
|
|
127
|
+
tooltip={false}
|
|
128
|
+
dateOptions={{ dateStyle: 'medium' }}
|
|
129
|
+
className="text-[13px] font-medium text-foreground"
|
|
130
|
+
/>
|
|
131
|
+
<Text as="span" variant="caption" tone="muted" className="font-mono">
|
|
132
|
+
v{e.version}
|
|
133
|
+
</Text>
|
|
134
|
+
</Stack>
|
|
135
|
+
<article aria-labelledby={`${countId}-${e.id}`}>
|
|
136
|
+
<Stack gap={3}>
|
|
137
|
+
<HStack gap={2} wrap>
|
|
138
|
+
{e.isNew && (
|
|
139
|
+
<Badge tone="primary" variant="solid">
|
|
140
|
+
New
|
|
141
|
+
</Badge>
|
|
142
|
+
)}
|
|
143
|
+
{e.tags.map((t) => (
|
|
144
|
+
<Badge key={t} tone={changelogTagTones[t]}>
|
|
145
|
+
{changelogTagLabels[t]}
|
|
146
|
+
</Badge>
|
|
147
|
+
))}
|
|
148
|
+
</HStack>
|
|
149
|
+
<Heading id={`${countId}-${e.id}`} level={2} size="sm">
|
|
150
|
+
{e.title}
|
|
151
|
+
</Heading>
|
|
152
|
+
<Text tone="muted">{e.summary}</Text>
|
|
153
|
+
<ul className="flex list-disc flex-col gap-1.5 ps-5 text-[13.5px] leading-6 text-foreground marker:text-muted-foreground">
|
|
154
|
+
{e.changes.map((c) => (
|
|
155
|
+
<li key={c}>{c}</li>
|
|
156
|
+
))}
|
|
157
|
+
</ul>
|
|
158
|
+
{e.href && (
|
|
159
|
+
<Link href={e.href} className="inline-flex items-center gap-1 self-start text-[13px]">
|
|
160
|
+
Read the release notes <ArrowRight size={14} aria-hidden />
|
|
161
|
+
<span className="sr-only"> for {e.version}</span>
|
|
162
|
+
</Link>
|
|
163
|
+
)}
|
|
164
|
+
</Stack>
|
|
165
|
+
</article>
|
|
166
|
+
</li>
|
|
167
|
+
))}
|
|
168
|
+
</ol>
|
|
169
|
+
)}
|
|
170
|
+
</Stack>
|
|
171
|
+
</Page>
|
|
172
|
+
</ConsoleShell>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { BadgeProps, BreadcrumbItem } from '@gntik-ai/ui';
|
|
2
|
+
|
|
3
|
+
export type ChangelogTag = 'feature' | 'improvement' | 'fix' | 'security' | 'deprecation';
|
|
4
|
+
|
|
5
|
+
export interface ChangelogEntry {
|
|
6
|
+
id: string;
|
|
7
|
+
/** Release version, e.g. "2.14". */
|
|
8
|
+
version: string;
|
|
9
|
+
date: string;
|
|
10
|
+
title: string;
|
|
11
|
+
summary: string;
|
|
12
|
+
/** Bullet list of changes. */
|
|
13
|
+
changes: string[];
|
|
14
|
+
tags: ChangelogTag[];
|
|
15
|
+
/** Shows the "New" badge (e.g. released since the viewer's last visit). */
|
|
16
|
+
isNew?: boolean;
|
|
17
|
+
/** "Read more" link. */
|
|
18
|
+
href?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const changelogTagLabels: Record<ChangelogTag, string> = {
|
|
22
|
+
feature: 'Feature',
|
|
23
|
+
improvement: 'Improvement',
|
|
24
|
+
fix: 'Fix',
|
|
25
|
+
security: 'Security',
|
|
26
|
+
deprecation: 'Deprecation',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const changelogTagTones: Record<ChangelogTag, NonNullable<BadgeProps['tone']>> = {
|
|
30
|
+
feature: 'primary',
|
|
31
|
+
improvement: 'info',
|
|
32
|
+
fix: 'neutral',
|
|
33
|
+
security: 'warning',
|
|
34
|
+
deprecation: 'destructive',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const changelogBreadcrumbs: BreadcrumbItem[] = [{ label: 'Help', href: '/help' }, { label: 'Changelog' }];
|
|
38
|
+
|
|
39
|
+
/** Neutral release notes, newest first. */
|
|
40
|
+
export const changelogEntries: ChangelogEntry[] = [
|
|
41
|
+
{
|
|
42
|
+
id: 'v2-14',
|
|
43
|
+
version: '2.14',
|
|
44
|
+
date: '2026-09-29',
|
|
45
|
+
title: 'Deploy gates and scheduled rollbacks',
|
|
46
|
+
summary: 'Block releases while an incident is open, and schedule a rollback for a quiet hour.',
|
|
47
|
+
changes: [
|
|
48
|
+
'Deploy gates check open incidents, failing checks and freeze windows before every release.',
|
|
49
|
+
'Rollbacks can be scheduled; the deployment page shows a countdown and a cancel action.',
|
|
50
|
+
'The deployments table remembers its density and hidden columns per project.',
|
|
51
|
+
],
|
|
52
|
+
tags: ['feature', 'improvement'],
|
|
53
|
+
isNew: true,
|
|
54
|
+
href: '/changelog/v2-14',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'v2-13-2',
|
|
58
|
+
version: '2.13.2',
|
|
59
|
+
date: '2026-09-18',
|
|
60
|
+
title: 'Stricter API key scopes',
|
|
61
|
+
summary: 'New keys default to read-only; existing keys keep their scopes.',
|
|
62
|
+
changes: [
|
|
63
|
+
'Keys created from now on start read-only.',
|
|
64
|
+
'Audit log entries now include the key’s last four characters.',
|
|
65
|
+
],
|
|
66
|
+
tags: ['security'],
|
|
67
|
+
isNew: true,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'v2-13-1',
|
|
71
|
+
version: '2.13.1',
|
|
72
|
+
date: '2026-09-07',
|
|
73
|
+
title: 'Fixes for usage exports',
|
|
74
|
+
summary: 'CSV exports respect the selected range and time zone again.',
|
|
75
|
+
changes: [
|
|
76
|
+
'Exports used UTC instead of the workspace time zone.',
|
|
77
|
+
'Large exports no longer time out after 30 seconds.',
|
|
78
|
+
],
|
|
79
|
+
tags: ['fix'],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'v2-13',
|
|
83
|
+
version: '2.13',
|
|
84
|
+
date: '2026-08-25',
|
|
85
|
+
title: 'Saved views for every table',
|
|
86
|
+
summary: 'Save filters, sorting and columns as a view, and share it with your workspace.',
|
|
87
|
+
changes: [
|
|
88
|
+
'Saved views on projects, deployments, members and invoices.',
|
|
89
|
+
'Views can be private or shared with the workspace.',
|
|
90
|
+
],
|
|
91
|
+
tags: ['feature'],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'v2-12',
|
|
95
|
+
version: '2.12',
|
|
96
|
+
date: '2026-08-04',
|
|
97
|
+
title: 'Legacy webhooks are deprecated',
|
|
98
|
+
summary: 'Version 1 webhooks stop on 1 December 2026. Move to signed webhooks before then.',
|
|
99
|
+
changes: [
|
|
100
|
+
'Signed webhooks include a timestamp and an HMAC header.',
|
|
101
|
+
'The integrations page lists every endpoint still on version 1.',
|
|
102
|
+
],
|
|
103
|
+
tags: ['deprecation', 'security'],
|
|
104
|
+
},
|
|
105
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TemplateMeta } from '../meta';
|
|
2
|
+
|
|
3
|
+
export const meta: TemplateMeta = {
|
|
4
|
+
name: 'Changelog',
|
|
5
|
+
family: 'Help',
|
|
6
|
+
priority: 'P3',
|
|
7
|
+
status: 'experimental',
|
|
8
|
+
description:
|
|
9
|
+
'Release notes in the console: dated entries with version, tag badges and a “New” badge, a multi-select tag filter (ToggleGroup) with a live count, and a Subscribe action.',
|
|
10
|
+
layout: 'SidebarLayout · Page',
|
|
11
|
+
blocks: ['app-sidebar', 'app-topbar', 'page-header'],
|
|
12
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import type { MonacoLoader } from '@gntik-ai/editor';
|
|
4
|
+
import { expectNoAxeViolations } from '../test/a11y';
|
|
5
|
+
import CodeEditorPage from './Page';
|
|
6
|
+
import { filesToTree } from './data';
|
|
7
|
+
|
|
8
|
+
/** Minimal Monaco stand-in (same approach as the CodePanel block tests). */
|
|
9
|
+
function mockMonaco() {
|
|
10
|
+
const model = (value: string) => ({
|
|
11
|
+
getValue: () => value,
|
|
12
|
+
setValue: vi.fn(),
|
|
13
|
+
onDidChangeContent: () => ({ dispose: vi.fn() }),
|
|
14
|
+
dispose: vi.fn(),
|
|
15
|
+
});
|
|
16
|
+
const editor = {
|
|
17
|
+
defineTheme: vi.fn(),
|
|
18
|
+
setTheme: vi.fn(),
|
|
19
|
+
setModelLanguage: vi.fn(),
|
|
20
|
+
remeasureFonts: vi.fn(),
|
|
21
|
+
createModel: vi.fn((value: string) => model(value)),
|
|
22
|
+
create: vi.fn((_el: HTMLElement, opts: { value?: string }) => {
|
|
23
|
+
const m = model(opts.value ?? '');
|
|
24
|
+
return {
|
|
25
|
+
getModel: () => m,
|
|
26
|
+
getValue: () => m.getValue(),
|
|
27
|
+
setValue: vi.fn(),
|
|
28
|
+
onDidChangeModelContent: () => ({ dispose: vi.fn() }),
|
|
29
|
+
updateOptions: vi.fn(),
|
|
30
|
+
layout: vi.fn(),
|
|
31
|
+
dispose: vi.fn(),
|
|
32
|
+
setPosition: vi.fn(),
|
|
33
|
+
revealLineInCenter: vi.fn(),
|
|
34
|
+
focus: vi.fn(),
|
|
35
|
+
};
|
|
36
|
+
}),
|
|
37
|
+
createDiffEditor: vi.fn(() => ({
|
|
38
|
+
setModel: vi.fn(),
|
|
39
|
+
getModel: () => null,
|
|
40
|
+
updateOptions: vi.fn(),
|
|
41
|
+
layout: vi.fn(),
|
|
42
|
+
dispose: vi.fn(),
|
|
43
|
+
})),
|
|
44
|
+
};
|
|
45
|
+
return { editor, loader: (() => Promise.resolve({ editor })) as unknown as MonacoLoader };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('CodeEditorPage', () => {
|
|
49
|
+
it('renders the editor chrome, file tree and code panel', { timeout: 15000 }, async () => {
|
|
50
|
+
const m = mockMonaco();
|
|
51
|
+
render(<CodeEditorPage loader={m.loader} />);
|
|
52
|
+
expect(screen.getByRole('main', { name: 'projects-api editor' })).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByRole('heading', { level: 1, name: 'projects-api' })).toBeInTheDocument();
|
|
54
|
+
expect(screen.getByRole('tree', { name: 'Files' })).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByRole('tab', { name: /handler\.ts/ })).toHaveAttribute('aria-selected', 'true');
|
|
56
|
+
expect(screen.getAllByRole('tab')).toHaveLength(2);
|
|
57
|
+
await waitFor(() => expect(m.editor.create).toHaveBeenCalled());
|
|
58
|
+
await expectNoAxeViolations();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('opens a file from the tree as the active tab', { timeout: 15000 }, async () => {
|
|
62
|
+
const m = mockMonaco();
|
|
63
|
+
render(<CodeEditorPage loader={m.loader} />);
|
|
64
|
+
await userEvent.click(within(screen.getByRole('treeitem', { name: /deploy\.yaml/ })).getByText('deploy.yaml'));
|
|
65
|
+
expect(screen.getAllByRole('tab')).toHaveLength(3);
|
|
66
|
+
expect(screen.getByRole('tab', { name: /deploy\.yaml/ })).toHaveAttribute('aria-selected', 'true');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('deletes a file from its context menu', { timeout: 15000 }, async () => {
|
|
70
|
+
const m = mockMonaco();
|
|
71
|
+
const onDeleteFile = vi.fn();
|
|
72
|
+
render(<CodeEditorPage loader={m.loader} onDeleteFile={onDeleteFile} />);
|
|
73
|
+
fireEvent.contextMenu(within(screen.getByRole('treeitem', { name: /query\.ts/ })).getByText('query.ts'));
|
|
74
|
+
await userEvent.click(await screen.findByRole('menuitem', { name: 'Delete file' }));
|
|
75
|
+
expect(onDeleteFile).toHaveBeenCalledWith('src/query.ts');
|
|
76
|
+
expect(screen.queryByRole('treeitem', { name: /query\.ts/ })).toBeNull();
|
|
77
|
+
expect(screen.getAllByRole('tab')).toHaveLength(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('builds a folders-first tree from paths', () => {
|
|
81
|
+
expect(filesToTree(['b.md', 'src/a.ts', 'src/lib/c.ts']).map((n) => n.id)).toEqual(['src/', 'b.md']);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { CodePanel, type CodeFile, type CodeProblem } from '@gntik-ai/blocks';
|
|
2
|
+
import type { MonacoLoader } from '@gntik-ai/editor';
|
|
3
|
+
import { Copy, ExternalLink, Save, Trash2, X } from '@gntik-ai/icons';
|
|
4
|
+
import {
|
|
5
|
+
Badge,
|
|
6
|
+
Breadcrumbs,
|
|
7
|
+
Button,
|
|
8
|
+
CanvasLayout,
|
|
9
|
+
ContextMenu,
|
|
10
|
+
ContextMenuContent,
|
|
11
|
+
ContextMenuTrigger,
|
|
12
|
+
MenuItem,
|
|
13
|
+
MenuSeparator,
|
|
14
|
+
StatusTag,
|
|
15
|
+
TreeList,
|
|
16
|
+
type BreadcrumbItem,
|
|
17
|
+
} from '@gntik-ai/ui';
|
|
18
|
+
import { useState, type MouseEvent } from 'react';
|
|
19
|
+
import { editorBreadcrumbs, editorDefaultOpen, editorFiles, editorProblems, editorProject, filesToTree } from './data';
|
|
20
|
+
|
|
21
|
+
export interface CodeEditorPageProps {
|
|
22
|
+
title: string;
|
|
23
|
+
/** StatusTag key next to the title. */
|
|
24
|
+
status: string;
|
|
25
|
+
/** Branch or version shown as a badge. */
|
|
26
|
+
branch: string;
|
|
27
|
+
files: CodeFile[];
|
|
28
|
+
problems: CodeProblem[];
|
|
29
|
+
/** Paths open as tabs initially (the first one is active). */
|
|
30
|
+
defaultOpen: string[];
|
|
31
|
+
/** Receives the edited files (path → new content). A returned promise shows the saving state. */
|
|
32
|
+
onSave: (changes: Record<string, string>) => void | Promise<void>;
|
|
33
|
+
/** Called after "Delete" in a file's context menu; the file leaves the tree and the tabs. */
|
|
34
|
+
onDeleteFile: (path: string) => void;
|
|
35
|
+
/** Monaco loader (self-hosting, tests). Monaco loads lazily by default. */
|
|
36
|
+
loader: MonacoLoader;
|
|
37
|
+
breadcrumbs: BreadcrumbItem[];
|
|
38
|
+
fullScreen: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Code editor page: CanvasLayout with a file TreeList (context menu: open, copy path, close,
|
|
43
|
+
* delete) in the palette and the CodePanel block (file tabs, editor / diff, problems) as the
|
|
44
|
+
* canvas. Opening a file from the tree focuses its tab; edits survive reopening and Save
|
|
45
|
+
* hands them over.
|
|
46
|
+
*/
|
|
47
|
+
export default function CodeEditorPage(props: Partial<CodeEditorPageProps>) {
|
|
48
|
+
const {
|
|
49
|
+
title = editorProject.title,
|
|
50
|
+
status = editorProject.status,
|
|
51
|
+
branch = editorProject.branch,
|
|
52
|
+
files: initialFiles = editorFiles,
|
|
53
|
+
problems = editorProblems,
|
|
54
|
+
defaultOpen = editorDefaultOpen,
|
|
55
|
+
onSave,
|
|
56
|
+
onDeleteFile,
|
|
57
|
+
loader,
|
|
58
|
+
breadcrumbs = editorBreadcrumbs,
|
|
59
|
+
fullScreen = true,
|
|
60
|
+
} = props;
|
|
61
|
+
const [deleted, setDeleted] = useState<string[]>([]);
|
|
62
|
+
const [open, setOpen] = useState(() => defaultOpen.filter((p) => initialFiles.some((f) => f.path === p)));
|
|
63
|
+
const [active, setActive] = useState(open[0] ?? '');
|
|
64
|
+
const [nonce, setNonce] = useState(0);
|
|
65
|
+
const [edits, setEdits] = useState<Record<string, string>>({});
|
|
66
|
+
const [saving, setSaving] = useState(false);
|
|
67
|
+
const [note, setNote] = useState('');
|
|
68
|
+
const [target, setTarget] = useState<string | null>(null);
|
|
69
|
+
|
|
70
|
+
const files = initialFiles.filter((f) => !deleted.includes(f.path));
|
|
71
|
+
const isFile = (id: string | null | undefined): id is string => !!id && files.some((f) => f.path === id);
|
|
72
|
+
const openFiles = open.flatMap((p) => {
|
|
73
|
+
const f = files.find((x) => x.path === p);
|
|
74
|
+
return f ? [{ ...f, content: edits[p] ?? f.content }] : [];
|
|
75
|
+
});
|
|
76
|
+
const dirty = Object.keys(edits).filter((p) => edits[p] !== files.find((f) => f.path === p)?.content);
|
|
77
|
+
|
|
78
|
+
const openFile = (path: string) => {
|
|
79
|
+
if (!isFile(path)) return;
|
|
80
|
+
setOpen((list) => (list.includes(path) ? list : [...list, path]));
|
|
81
|
+
setActive(path);
|
|
82
|
+
// CodePanel only takes an initial file: remount it on the requested tab (edits live here).
|
|
83
|
+
setNonce((n) => n + 1);
|
|
84
|
+
};
|
|
85
|
+
const closeFile = (path: string) => {
|
|
86
|
+
const rest = open.filter((p) => p !== path);
|
|
87
|
+
setOpen(rest);
|
|
88
|
+
if (active === path) setActive(rest[0] ?? '');
|
|
89
|
+
setNonce((n) => n + 1);
|
|
90
|
+
};
|
|
91
|
+
const deleteFile = (path: string) => {
|
|
92
|
+
closeFile(path);
|
|
93
|
+
setDeleted((d) => [...d, path]);
|
|
94
|
+
onDeleteFile?.(path);
|
|
95
|
+
setNote(`Deleted ${path}`);
|
|
96
|
+
};
|
|
97
|
+
const copyPath = async (path: string) => {
|
|
98
|
+
try {
|
|
99
|
+
await navigator.clipboard.writeText(path);
|
|
100
|
+
setNote(`Copied ${path}`);
|
|
101
|
+
} catch {
|
|
102
|
+
setNote('Copy failed');
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const save = async () => {
|
|
106
|
+
setSaving(true);
|
|
107
|
+
try {
|
|
108
|
+
await onSave?.(Object.fromEntries(dirty.map((p) => [p, edits[p] ?? ''])));
|
|
109
|
+
setNote('All changes saved');
|
|
110
|
+
} finally {
|
|
111
|
+
setSaving(false);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Right click: the row under the pointer. Keyboard (Shift+F10): the focused row.
|
|
116
|
+
const pickTarget = (event: MouseEvent<HTMLElement>) => {
|
|
117
|
+
const fromPointer = (event.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]');
|
|
118
|
+
const fromFocus = document.activeElement?.closest<HTMLElement>('[role="treeitem"]');
|
|
119
|
+
setTarget((fromPointer ?? fromFocus)?.dataset.id ?? null);
|
|
120
|
+
};
|
|
121
|
+
const menuFile = isFile(target) ? target : null;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<CanvasLayout
|
|
125
|
+
fullScreen={fullScreen}
|
|
126
|
+
mainLabel={`${title} editor`}
|
|
127
|
+
smallScreenNotice={null}
|
|
128
|
+
paletteLabel="Files"
|
|
129
|
+
palette={
|
|
130
|
+
<ContextMenu>
|
|
131
|
+
<ContextMenuTrigger
|
|
132
|
+
tabIndex={-1}
|
|
133
|
+
role="group"
|
|
134
|
+
aria-label="Project files"
|
|
135
|
+
hint="Shift+F10 opens file actions"
|
|
136
|
+
onContextMenu={pickTarget}
|
|
137
|
+
className="block border-0 bg-transparent p-0"
|
|
138
|
+
>
|
|
139
|
+
<TreeList
|
|
140
|
+
aria-label="Files"
|
|
141
|
+
nodes={filesToTree(files.map((f) => f.path))}
|
|
142
|
+
defaultExpanded={['src/', 'config/']}
|
|
143
|
+
selected={active ? [active] : []}
|
|
144
|
+
onSelectedChange={(ids) => ids[0] && openFile(ids[0])}
|
|
145
|
+
onAction={(node) => openFile(node.id)}
|
|
146
|
+
/>
|
|
147
|
+
</ContextMenuTrigger>
|
|
148
|
+
<ContextMenuContent className="w-52">
|
|
149
|
+
<MenuItem icon={ExternalLink} disabled={!menuFile} onClick={() => menuFile && openFile(menuFile)}>
|
|
150
|
+
Open
|
|
151
|
+
</MenuItem>
|
|
152
|
+
<MenuItem icon={Copy} disabled={!menuFile} onClick={() => menuFile && void copyPath(menuFile)}>
|
|
153
|
+
Copy path
|
|
154
|
+
</MenuItem>
|
|
155
|
+
<MenuItem
|
|
156
|
+
icon={X}
|
|
157
|
+
disabled={!menuFile || !open.includes(menuFile)}
|
|
158
|
+
onClick={() => menuFile && closeFile(menuFile)}
|
|
159
|
+
>
|
|
160
|
+
Close tab
|
|
161
|
+
</MenuItem>
|
|
162
|
+
<MenuSeparator />
|
|
163
|
+
<MenuItem icon={Trash2} destructive disabled={!menuFile} onClick={() => menuFile && deleteFile(menuFile)}>
|
|
164
|
+
Delete file
|
|
165
|
+
</MenuItem>
|
|
166
|
+
</ContextMenuContent>
|
|
167
|
+
</ContextMenu>
|
|
168
|
+
}
|
|
169
|
+
header={
|
|
170
|
+
<>
|
|
171
|
+
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
172
|
+
<Breadcrumbs items={breadcrumbs} className="hidden min-w-0 md:flex" />
|
|
173
|
+
<h1 className="truncate font-mono text-[14px] font-semibold text-foreground">{title}</h1>
|
|
174
|
+
<StatusTag status={status} />
|
|
175
|
+
<Badge size="sm" className="hidden font-mono sm:inline-flex">
|
|
176
|
+
{branch}
|
|
177
|
+
</Badge>
|
|
178
|
+
<p aria-live="polite" className="hidden truncate text-[12px] text-muted-foreground sm:block">
|
|
179
|
+
{note || (dirty.length ? `${dirty.length} unsaved ${dirty.length === 1 ? 'file' : 'files'}` : '')}
|
|
180
|
+
</p>
|
|
181
|
+
</div>
|
|
182
|
+
<Button size="sm" icon={Save} loading={saving} disabled={dirty.length === 0} onClick={() => void save()}>
|
|
183
|
+
Save
|
|
184
|
+
</Button>
|
|
185
|
+
</>
|
|
186
|
+
}
|
|
187
|
+
>
|
|
188
|
+
<div className="h-full overflow-auto p-3 sm:p-4">
|
|
189
|
+
{/* CodePanel titles its problems list h3 and has no heading level prop. */}
|
|
190
|
+
<h2 className="sr-only">Open files</h2>
|
|
191
|
+
{openFiles.length === 0 ? (
|
|
192
|
+
<p className="p-6 text-center text-[13px] text-muted-foreground">Open a file from the tree to edit it.</p>
|
|
193
|
+
) : (
|
|
194
|
+
<CodePanel
|
|
195
|
+
key={`${nonce}`}
|
|
196
|
+
files={openFiles}
|
|
197
|
+
problems={problems.filter((p) => open.includes(p.file))}
|
|
198
|
+
defaultFile={active}
|
|
199
|
+
height={420}
|
|
200
|
+
loader={loader}
|
|
201
|
+
onChange={(path, value) => setEdits((e) => ({ ...e, [path]: value }))}
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</CanvasLayout>
|
|
206
|
+
);
|
|
207
|
+
}
|