@checkstack/ui 1.11.0 → 1.13.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/.storybook/main.ts +43 -0
- package/CHANGELOG.md +326 -0
- package/package.json +23 -18
- package/scripts/generate-stdlib-types.ts +23 -0
- package/src/components/Accordion.tsx +17 -9
- package/src/components/ActionCard.tsx +99 -11
- package/src/components/BrandIcon.tsx +57 -0
- package/src/components/CodeEditor/CodeEditor.tsx +159 -14
- package/src/components/CodeEditor/TypefoxEditor.tsx +537 -168
- package/src/components/CodeEditor/editorTheme.test.ts +41 -0
- package/src/components/CodeEditor/editorTheme.ts +26 -0
- package/src/components/CodeEditor/generated/builtin-modules.json +1 -0
- package/src/components/CodeEditor/importSpecifiers.test.ts +286 -0
- package/src/components/CodeEditor/importSpecifiers.ts +267 -0
- package/src/components/CodeEditor/index.ts +26 -0
- package/src/components/CodeEditor/monacoGuard.ts +76 -0
- package/src/components/CodeEditor/monacoTsService.ts +185 -0
- package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
- package/src/components/CodeEditor/popoutTitle.ts +31 -0
- package/src/components/CodeEditor/scriptContext.test.ts +15 -7
- package/src/components/CodeEditor/scriptContext.ts +12 -18
- package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
- package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
- package/src/components/CodeEditor/types.ts +79 -0
- package/src/components/CodeEditor/validateScripts.ts +172 -0
- package/src/components/CodeEditor/vscodeServicesSignal.ts +72 -0
- package/src/components/ConfirmationModal.tsx +7 -1
- package/src/components/Dialog.tsx +32 -11
- package/src/components/DurationInput.tsx +121 -0
- package/src/components/DynamicForm/DynamicForm.tsx +119 -47
- package/src/components/DynamicForm/DynamicOptionsField.tsx +19 -14
- package/src/components/DynamicForm/FormField.tsx +183 -15
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +78 -2
- package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
- package/src/components/DynamicForm/index.ts +20 -0
- package/src/components/DynamicForm/secretEnv.logic.test.ts +126 -0
- package/src/components/DynamicForm/secretEnv.logic.ts +87 -0
- package/src/components/DynamicForm/types.ts +134 -1
- package/src/components/DynamicForm/utils.test.ts +38 -0
- package/src/components/DynamicForm/utils.ts +54 -0
- package/src/components/DynamicForm/validation.logic.test.ts +255 -0
- package/src/components/DynamicForm/validation.logic.ts +210 -0
- package/src/components/DynamicIcon.tsx +39 -17
- package/src/components/Markdown.tsx +68 -2
- package/src/components/Popover.tsx +6 -1
- package/src/components/ScriptTestPanel.logic.test.ts +139 -0
- package/src/components/ScriptTestPanel.logic.ts +137 -0
- package/src/components/ScriptTestPanel.tsx +394 -0
- package/src/components/Sheet.tsx +21 -6
- package/src/components/Spinner.tsx +56 -0
- package/src/components/StatusBadge.tsx +78 -0
- package/src/components/StrategyConfigCard.tsx +3 -3
- package/src/components/Tabs.tsx +7 -1
- package/src/components/TimeOfDayInput.tsx +116 -0
- package/src/components/UserMenu.logic.test.ts +37 -0
- package/src/components/UserMenu.logic.ts +30 -0
- package/src/components/UserMenu.tsx +40 -12
- package/src/components/comboboxInteraction.ts +39 -0
- package/src/components/iconRegistry.tsx +27 -0
- package/src/components/portalContainer.ts +24 -0
- package/src/index.ts +7 -0
- package/stories/ActionCard.stories.tsx +60 -0
- package/stories/CodeEditor.stories.tsx +47 -2
- package/stories/DurationInput.stories.tsx +59 -0
- package/stories/Introduction.mdx +1 -1
- package/stories/Markdown.stories.tsx +56 -0
- package/stories/ScriptTestPanel.stories.tsx +106 -0
- package/stories/SecretEnvEditor.stories.tsx +80 -0
- package/stories/Spinner.stories.tsx +90 -0
- package/stories/TimeOfDayInput.stories.tsx +34 -0
- package/tsconfig.json +4 -0
|
@@ -4,15 +4,31 @@ import {
|
|
|
4
4
|
ChevronRight,
|
|
5
5
|
GripVertical,
|
|
6
6
|
AlertTriangle,
|
|
7
|
+
MoreVertical,
|
|
7
8
|
Trash2,
|
|
8
9
|
} from "lucide-react";
|
|
9
10
|
import { Card, CardContent, CardHeader } from "./Card";
|
|
10
11
|
import { Button } from "./Button";
|
|
11
12
|
import { Toggle } from "./Toggle";
|
|
12
|
-
import { DynamicIcon, type
|
|
13
|
+
import { DynamicIcon, type IconName } from "./DynamicIcon";
|
|
13
14
|
import { Badge, type BadgeProps } from "./Badge";
|
|
15
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./Popover";
|
|
16
|
+
import { DropdownMenuItem, MenuCloseContext } from "./Menu";
|
|
14
17
|
import { cn } from "../utils";
|
|
15
18
|
|
|
19
|
+
/**
|
|
20
|
+
* A single entry in an {@link ActionCard}'s three-dot overflow menu. Used to
|
|
21
|
+
* relocate per-card commands (Duplicate, Disable/Enable, Delete) off the
|
|
22
|
+
* header row into a compact menu for a Home-Assistant-style collapsed card.
|
|
23
|
+
*/
|
|
24
|
+
export interface ActionCardMenuItem {
|
|
25
|
+
label: string;
|
|
26
|
+
icon?: IconName;
|
|
27
|
+
onClick: () => void;
|
|
28
|
+
/** `destructive` tints the item red (e.g. Delete). Defaults to neutral. */
|
|
29
|
+
variant?: "default" | "destructive";
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
export interface ActionCardProps {
|
|
17
33
|
/** Stable identifier used for drag-reorder + React key. */
|
|
18
34
|
id: string;
|
|
@@ -22,8 +38,8 @@ export interface ActionCardProps {
|
|
|
22
38
|
description?: string;
|
|
23
39
|
/** Plugin/category label rendered as a subdued badge. */
|
|
24
40
|
category?: string;
|
|
25
|
-
/**
|
|
26
|
-
icon?:
|
|
41
|
+
/** Icon (PascalCase) shown to the left of the title - lucide or brand. */
|
|
42
|
+
icon?: IconName;
|
|
27
43
|
/** Toggle for the action's `enabled` flag. Omit to hide the toggle. */
|
|
28
44
|
enabled?: boolean;
|
|
29
45
|
onEnabledChange?: (enabled: boolean) => void;
|
|
@@ -36,6 +52,26 @@ export interface ActionCardProps {
|
|
|
36
52
|
/** Controlled expanded state. */
|
|
37
53
|
expanded?: boolean;
|
|
38
54
|
onExpandedChange?: (expanded: boolean) => void;
|
|
55
|
+
/**
|
|
56
|
+
* When provided, the card behaves as a Home-Assistant-style collapsed
|
|
57
|
+
* summary row: clicking the header opens the item's full configuration in
|
|
58
|
+
* a side sheet (via this callback) instead of expanding inline. The body
|
|
59
|
+
* `children` are not rendered inline in this mode - the host renders them
|
|
60
|
+
* inside its own sheet. Omit to keep the legacy inline-expand behaviour.
|
|
61
|
+
*/
|
|
62
|
+
onOpenSheet?: () => void;
|
|
63
|
+
/**
|
|
64
|
+
* Compact, single-line summary shown under the title in the collapsed
|
|
65
|
+
* summary row (e.g. derived from the item's config). Distinct from
|
|
66
|
+
* `description`, which is the operator's free-text note; `summary` wins
|
|
67
|
+
* when both are present.
|
|
68
|
+
*/
|
|
69
|
+
summary?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Overflow-menu commands rendered behind a three-dot button on the header
|
|
72
|
+
* row (Duplicate / Disable / Delete, etc.). Omit to hide the menu.
|
|
73
|
+
*/
|
|
74
|
+
actions?: ActionCardMenuItem[];
|
|
39
75
|
/** Extra badges (e.g. produces / consumes hints). */
|
|
40
76
|
badges?: Array<{
|
|
41
77
|
label: string;
|
|
@@ -101,6 +137,9 @@ export const ActionCard: React.FC<ActionCardProps> = ({
|
|
|
101
137
|
defaultExpanded = true,
|
|
102
138
|
expanded,
|
|
103
139
|
onExpandedChange,
|
|
140
|
+
onOpenSheet,
|
|
141
|
+
summary,
|
|
142
|
+
actions,
|
|
104
143
|
badges,
|
|
105
144
|
children,
|
|
106
145
|
className,
|
|
@@ -108,8 +147,16 @@ export const ActionCard: React.FC<ActionCardProps> = ({
|
|
|
108
147
|
}) => {
|
|
109
148
|
const [internalExpanded, setInternalExpanded] =
|
|
110
149
|
React.useState(defaultExpanded);
|
|
111
|
-
const
|
|
112
|
-
|
|
150
|
+
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
151
|
+
// Sheet mode: the header opens a side sheet instead of expanding inline,
|
|
152
|
+
// so the card stays a compact summary row at all times.
|
|
153
|
+
const sheetMode = onOpenSheet !== undefined;
|
|
154
|
+
const isExpanded = sheetMode ? false : (expanded ?? internalExpanded);
|
|
155
|
+
const handleHeaderClick = () => {
|
|
156
|
+
if (sheetMode) {
|
|
157
|
+
onOpenSheet();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
113
160
|
const next = !isExpanded;
|
|
114
161
|
if (expanded === undefined) setInternalExpanded(next);
|
|
115
162
|
onExpandedChange?.(next);
|
|
@@ -139,12 +186,14 @@ export const ActionCard: React.FC<ActionCardProps> = ({
|
|
|
139
186
|
)}
|
|
140
187
|
<button
|
|
141
188
|
type="button"
|
|
142
|
-
onClick={
|
|
189
|
+
onClick={handleHeaderClick}
|
|
143
190
|
className="flex items-center flex-1 gap-2 text-left"
|
|
144
|
-
aria-expanded={isExpanded}
|
|
145
|
-
aria-controls={`${id}-body`}
|
|
191
|
+
aria-expanded={sheetMode ? undefined : isExpanded}
|
|
192
|
+
aria-controls={sheetMode ? undefined : `${id}-body`}
|
|
146
193
|
>
|
|
147
|
-
{
|
|
194
|
+
{sheetMode ? (
|
|
195
|
+
<ChevronRight className="w-4 h-4 shrink-0 text-muted-foreground" />
|
|
196
|
+
) : isExpanded ? (
|
|
148
197
|
<ChevronDown className="w-4 h-4 shrink-0 text-muted-foreground" />
|
|
149
198
|
) : (
|
|
150
199
|
<ChevronRight className="w-4 h-4 shrink-0 text-muted-foreground" />
|
|
@@ -172,9 +221,9 @@ export const ActionCard: React.FC<ActionCardProps> = ({
|
|
|
172
221
|
</Badge>
|
|
173
222
|
))}
|
|
174
223
|
</div>
|
|
175
|
-
{description && (
|
|
224
|
+
{(summary ?? description) && (
|
|
176
225
|
<p className="text-xs truncate text-muted-foreground">
|
|
177
|
-
{description}
|
|
226
|
+
{summary ?? description}
|
|
178
227
|
</p>
|
|
179
228
|
)}
|
|
180
229
|
{hasErrors && (
|
|
@@ -198,6 +247,45 @@ export const ActionCard: React.FC<ActionCardProps> = ({
|
|
|
198
247
|
aria-label={enabled ? "Disable action" : "Enable action"}
|
|
199
248
|
/>
|
|
200
249
|
)}
|
|
250
|
+
{actions && actions.length > 0 && (
|
|
251
|
+
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
|
|
252
|
+
<PopoverTrigger asChild>
|
|
253
|
+
<Button
|
|
254
|
+
type="button"
|
|
255
|
+
variant="ghost"
|
|
256
|
+
size="icon"
|
|
257
|
+
className="w-8 h-8 text-muted-foreground hover:text-foreground shrink-0"
|
|
258
|
+
aria-label="More actions"
|
|
259
|
+
>
|
|
260
|
+
<MoreVertical className="w-4 h-4" />
|
|
261
|
+
</Button>
|
|
262
|
+
</PopoverTrigger>
|
|
263
|
+
<PopoverContent align="end" className="w-48 p-1">
|
|
264
|
+
<MenuCloseContext.Provider
|
|
265
|
+
value={{ onClose: () => setMenuOpen(false) }}
|
|
266
|
+
>
|
|
267
|
+
{actions.map((item) => (
|
|
268
|
+
<DropdownMenuItem
|
|
269
|
+
key={item.label}
|
|
270
|
+
onClick={item.onClick}
|
|
271
|
+
icon={
|
|
272
|
+
item.icon ? (
|
|
273
|
+
<DynamicIcon name={item.icon} className="w-4 h-4" />
|
|
274
|
+
) : undefined
|
|
275
|
+
}
|
|
276
|
+
className={
|
|
277
|
+
item.variant === "destructive"
|
|
278
|
+
? "text-destructive hover:text-destructive"
|
|
279
|
+
: undefined
|
|
280
|
+
}
|
|
281
|
+
>
|
|
282
|
+
{item.label}
|
|
283
|
+
</DropdownMenuItem>
|
|
284
|
+
))}
|
|
285
|
+
</MenuCloseContext.Provider>
|
|
286
|
+
</PopoverContent>
|
|
287
|
+
</Popover>
|
|
288
|
+
)}
|
|
201
289
|
{onDelete && (
|
|
202
290
|
<Button
|
|
203
291
|
type="button"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { SVGProps } from "react";
|
|
2
|
+
import type { BrandIconName } from "@checkstack/common";
|
|
3
|
+
|
|
4
|
+
// Brand marks vendored as inline SVGs.
|
|
5
|
+
//
|
|
6
|
+
// lucide-react v1 removed ALL brand icons (GitHub, GitLab, ...) for trademark
|
|
7
|
+
// reasons, so we ship the handful we actually need ourselves. Path data is the
|
|
8
|
+
// official mark from Simple Icons (https://simpleicons.org), ISC/CC0. Props
|
|
9
|
+
// mirror the slice of the lucide icon surface our call sites use (`className` +
|
|
10
|
+
// pass-through SVG props), so these are drop-in replacements for the old
|
|
11
|
+
// `import { Github } from "lucide-react"`. `fill="currentColor"` makes them
|
|
12
|
+
// inherit text color like a lucide icon does via `stroke="currentColor"`.
|
|
13
|
+
|
|
14
|
+
type BrandIconProps = SVGProps<SVGSVGElement>;
|
|
15
|
+
|
|
16
|
+
export const GithubIcon = ({ className, ...props }: BrandIconProps) => (
|
|
17
|
+
<svg
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
width={24}
|
|
20
|
+
height={24}
|
|
21
|
+
fill="currentColor"
|
|
22
|
+
className={className}
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const GitlabIcon = ({ className, ...props }: BrandIconProps) => (
|
|
31
|
+
<svg
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
width={24}
|
|
34
|
+
height={24}
|
|
35
|
+
fill="currentColor"
|
|
36
|
+
className={className}
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
<path d="m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z" />
|
|
41
|
+
</svg>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Brand-name -> component map. Used by `DynamicIcon` to resolve a data-driven
|
|
46
|
+
* icon name (e.g. an auth strategy's `icon: "Github"`) to a vendored brand
|
|
47
|
+
* mark, since these names no longer exist in lucide's `icons` export. Keys are
|
|
48
|
+
* the `BrandIconName` union from `@checkstack/common`, so adding a brand there
|
|
49
|
+
* without a component here is a type error.
|
|
50
|
+
*/
|
|
51
|
+
export const brandIcons: Record<
|
|
52
|
+
BrandIconName,
|
|
53
|
+
(props: BrandIconProps) => React.JSX.Element
|
|
54
|
+
> = {
|
|
55
|
+
Github: GithubIcon,
|
|
56
|
+
Gitlab: GitlabIcon,
|
|
57
|
+
};
|
|
@@ -2,12 +2,77 @@
|
|
|
2
2
|
// `@typefox/monaco-editor-react`-backed `TypefoxEditor` (real VS Code language
|
|
3
3
|
// services in the browser). Consumers (DynamicForm, automation, healthcheck)
|
|
4
4
|
// import this and are unaffected by the underlying editor implementation.
|
|
5
|
-
import {
|
|
5
|
+
import { useEffect, useState, type ComponentType } from "react";
|
|
6
|
+
import { Maximize2 } from "lucide-react";
|
|
7
|
+
import type { TypefoxEditorProps } from "./TypefoxEditor";
|
|
8
|
+
import { popoutTitle } from "./popoutTitle";
|
|
6
9
|
import type { CodeEditorProps } from "./types";
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
} from "../Dialog";
|
|
16
|
+
import { Skeleton } from "../Skeleton";
|
|
17
|
+
import { usePerformance } from "../PerformanceProvider";
|
|
18
|
+
import { cn } from "../../utils";
|
|
19
|
+
|
|
20
|
+
// Load the Monaco-backed editor chunk on demand WITHOUT React.lazy/Suspense.
|
|
21
|
+
//
|
|
22
|
+
// The `import("./TypefoxEditor")` is still dynamic, so the whole `@codingame/*`
|
|
23
|
+
// / Monaco stack stays in its own chunk and off pages that merely import
|
|
24
|
+
// primitives from the `@checkstack/ui` barrel (e.g. the login page) - the
|
|
25
|
+
// bundle-split goal is preserved. But we deliberately do NOT use `React.lazy` +
|
|
26
|
+
// `<Suspense>`: suspending THROUGH `@typefox`'s `MonacoEditorReactComp` makes
|
|
27
|
+
// React (under StrictMode's mount -> unmount -> remount) re-run the wrapper's
|
|
28
|
+
// ONE-SHOT global monaco-vscode init against already-initialized services,
|
|
29
|
+
// throwing "Services are already initialized" so the editor never reaches
|
|
30
|
+
// `onEditorStartDone` and never renders (dev-only; the prod Rollup build is
|
|
31
|
+
// fine). The wrapper expects a stable, non-suspending parent. So we resolve the
|
|
32
|
+
// chunk imperatively and render the wrapper only once it's loaded - a plain,
|
|
33
|
+
// synchronous mount, exactly as the pre-#236 static import behaved.
|
|
34
|
+
let editorModulePromise: Promise<typeof import("./TypefoxEditor")> | undefined;
|
|
35
|
+
const loadEditorModule = (): Promise<typeof import("./TypefoxEditor")> =>
|
|
36
|
+
(editorModulePromise ??= import("./TypefoxEditor"));
|
|
37
|
+
|
|
38
|
+
const useTypefoxEditor = (): ComponentType<TypefoxEditorProps> | null => {
|
|
39
|
+
const [Editor, setEditor] = useState<ComponentType<TypefoxEditorProps> | null>(
|
|
40
|
+
null,
|
|
41
|
+
);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
let active = true;
|
|
44
|
+
void loadEditorModule().then((mod) => {
|
|
45
|
+
if (active) setEditor(() => mod.TypefoxEditor);
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
active = false;
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
return Editor;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Placeholder shown while the Monaco chunk loads. A plain `Skeleton` block
|
|
56
|
+
* (which already honours `usePerformance` / `isLowPower`) sized to the editor's
|
|
57
|
+
* height, so the layout doesn't jump and low-power devices avoid a heavy
|
|
58
|
+
* spinner.
|
|
59
|
+
*/
|
|
60
|
+
const EditorLoadingFallback = ({ minHeightPx }: { minHeightPx: number }) => (
|
|
61
|
+
<Skeleton
|
|
62
|
+
className="w-full rounded-md"
|
|
63
|
+
style={{ height: `${minHeightPx}px` }}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
7
66
|
|
|
8
67
|
/**
|
|
9
68
|
* Code editor with context-aware IntelliSense, template / shell completion, and
|
|
10
69
|
* structural validation. See `CodeEditorProps`.
|
|
70
|
+
*
|
|
71
|
+
* Renders the inline editor with a subtle top-right "expand" affordance that
|
|
72
|
+
* opens the SAME editor (same `value` / `onChange` / completion props) in a
|
|
73
|
+
* large full-screen overlay for comfortably editing big scripts. Both the
|
|
74
|
+
* inline and overlay editors are `TypefoxEditor` instances bound to the same
|
|
75
|
+
* controlled `value`, so edits stay in sync and closing the overlay keeps them.
|
|
11
76
|
*/
|
|
12
77
|
export const CodeEditor = ({
|
|
13
78
|
id,
|
|
@@ -21,25 +86,103 @@ export const CodeEditor = ({
|
|
|
21
86
|
templateProperties,
|
|
22
87
|
shellEnvVars,
|
|
23
88
|
markers,
|
|
89
|
+
acquireTypes,
|
|
90
|
+
acquireResetKey,
|
|
91
|
+
sdkTypes,
|
|
92
|
+
sdkTypesResetKey,
|
|
93
|
+
importablePackages,
|
|
94
|
+
allowPopout = true,
|
|
95
|
+
title,
|
|
96
|
+
deferInit,
|
|
24
97
|
}: CodeEditorProps) => {
|
|
98
|
+
const [popoutOpen, setPopoutOpen] = useState(false);
|
|
99
|
+
const { isLowPower } = usePerformance();
|
|
100
|
+
// Resolved Monaco editor component (null until its chunk loads). Loaded
|
|
101
|
+
// imperatively rather than via React.lazy/Suspense - see loadEditorModule.
|
|
102
|
+
const TypefoxEditor = useTypefoxEditor();
|
|
103
|
+
|
|
25
104
|
// CodeEditorProps.minHeight is a CSS length string ("240px"); TypefoxEditor
|
|
26
105
|
// takes a pixel number.
|
|
27
106
|
const minHeightPx = Number.parseInt(minHeight, 10) || 100;
|
|
107
|
+
const editorId = id ?? "code-editor";
|
|
108
|
+
|
|
109
|
+
// Shared editor props for both the inline and overlay instances. The overlay
|
|
110
|
+
// instance overrides only `id` (a distinct model URI so the two Monaco models
|
|
111
|
+
// don't fight) and `fillHeight` (so it fills the tall dialog body). Both are
|
|
112
|
+
// bound to the same `value` / `onChange`, keeping edits in sync.
|
|
113
|
+
const sharedEditorProps: Omit<TypefoxEditorProps, "id" | "fillHeight"> = {
|
|
114
|
+
value,
|
|
115
|
+
onChange,
|
|
116
|
+
language,
|
|
117
|
+
minHeight: minHeightPx,
|
|
118
|
+
readOnly,
|
|
119
|
+
placeholder,
|
|
120
|
+
typeDefinitions,
|
|
121
|
+
templateProperties,
|
|
122
|
+
shellEnvVars,
|
|
123
|
+
markers,
|
|
124
|
+
acquireTypes,
|
|
125
|
+
acquireResetKey,
|
|
126
|
+
sdkTypes,
|
|
127
|
+
sdkTypesResetKey,
|
|
128
|
+
importablePackages,
|
|
129
|
+
deferInit,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const dialogTitle = title ?? popoutTitle({ language });
|
|
28
133
|
|
|
29
134
|
return (
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
135
|
+
<div className="relative">
|
|
136
|
+
{TypefoxEditor ? (
|
|
137
|
+
<TypefoxEditor id={editorId} {...sharedEditorProps} />
|
|
138
|
+
) : (
|
|
139
|
+
<EditorLoadingFallback minHeightPx={minHeightPx} />
|
|
140
|
+
)}
|
|
141
|
+
{allowPopout && (
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
aria-label="Expand editor"
|
|
145
|
+
title="Expand editor"
|
|
146
|
+
onClick={() => setPopoutOpen(true)}
|
|
147
|
+
className={cn(
|
|
148
|
+
// Sits above the editor in the top-right corner. A faint background
|
|
149
|
+
// keeps the muted icon legible over code; hover emphasises it.
|
|
150
|
+
"absolute right-2 top-2 z-10 inline-flex h-7 w-7 items-center justify-center",
|
|
151
|
+
"rounded-md bg-background/70 text-muted-foreground",
|
|
152
|
+
!isLowPower && "backdrop-blur-sm",
|
|
153
|
+
"transition-colors hover:bg-accent hover:text-accent-foreground",
|
|
154
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
<Maximize2 className="h-4 w-4" />
|
|
158
|
+
</button>
|
|
159
|
+
)}
|
|
160
|
+
<Dialog open={popoutOpen} onOpenChange={setPopoutOpen}>
|
|
161
|
+
{/* Tall flex column: override the default `overflow-y-auto` so the
|
|
162
|
+
editor (not the dialog) scrolls internally, and give the body a
|
|
163
|
+
fixed tall height so the `fillHeight` editor has something to fill. */}
|
|
164
|
+
<DialogContent
|
|
165
|
+
size="full"
|
|
166
|
+
className="flex h-[85dvh] flex-col overflow-hidden overflow-y-hidden"
|
|
167
|
+
>
|
|
168
|
+
<DialogHeader>
|
|
169
|
+
<DialogTitle>{dialogTitle}</DialogTitle>
|
|
170
|
+
</DialogHeader>
|
|
171
|
+
{/* The second Monaco instance only mounts while the dialog is open,
|
|
172
|
+
so there's no double-editor cost when closed. Distinct
|
|
173
|
+
`${id}-popout` id => distinct model URI. */}
|
|
174
|
+
{popoutOpen && TypefoxEditor && (
|
|
175
|
+
<div className="flex-1 min-h-0">
|
|
176
|
+
<TypefoxEditor
|
|
177
|
+
id={`${editorId}-popout`}
|
|
178
|
+
fillHeight
|
|
179
|
+
{...sharedEditorProps}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</DialogContent>
|
|
184
|
+
</Dialog>
|
|
185
|
+
</div>
|
|
43
186
|
);
|
|
44
187
|
};
|
|
45
188
|
|
|
@@ -49,6 +192,8 @@ export type {
|
|
|
49
192
|
TemplateProperty,
|
|
50
193
|
ShellEnvVar,
|
|
51
194
|
EditorMarker,
|
|
195
|
+
AcquireTypes,
|
|
196
|
+
AcquiredTypeFile,
|
|
52
197
|
} from "./types";
|
|
53
198
|
|
|
54
199
|
export {
|