@checkstack/ui 1.10.0 → 1.12.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.
Files changed (72) hide show
  1. package/.storybook/main.ts +43 -0
  2. package/CHANGELOG.md +565 -0
  3. package/package.json +15 -7
  4. package/scripts/generate-stdlib-types.ts +25 -2
  5. package/src/components/ActionCard.tsx +309 -0
  6. package/src/components/CodeEditor/CodeEditor.tsx +132 -9
  7. package/src/components/CodeEditor/TypefoxEditor.tsx +1024 -0
  8. package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
  9. package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
  10. package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
  11. package/src/components/CodeEditor/generated/builtin-modules.json +1 -0
  12. package/src/components/CodeEditor/importSpecifiers.test.ts +286 -0
  13. package/src/components/CodeEditor/importSpecifiers.ts +267 -0
  14. package/src/components/CodeEditor/index.ts +26 -0
  15. package/src/components/CodeEditor/monacoTsService.ts +217 -0
  16. package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
  17. package/src/components/CodeEditor/popoutTitle.ts +31 -0
  18. package/src/components/CodeEditor/scriptContext.test.ts +41 -0
  19. package/src/components/CodeEditor/scriptContext.ts +76 -1
  20. package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
  21. package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
  22. package/src/components/CodeEditor/templateValidation.ts +51 -0
  23. package/src/components/CodeEditor/types.ts +168 -0
  24. package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
  25. package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
  26. package/src/components/CodeEditor/validateScripts.ts +132 -0
  27. package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
  28. package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
  29. package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
  30. package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
  31. package/src/components/Dialog.tsx +32 -11
  32. package/src/components/DurationInput.tsx +121 -0
  33. package/src/components/DynamicForm/DynamicForm.tsx +27 -1
  34. package/src/components/DynamicForm/FormField.tsx +138 -10
  35. package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
  36. package/src/components/DynamicForm/MultiTypeEditorField.tsx +83 -9
  37. package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
  38. package/src/components/DynamicForm/index.ts +6 -0
  39. package/src/components/DynamicForm/secretEnv.logic.test.ts +126 -0
  40. package/src/components/DynamicForm/secretEnv.logic.ts +87 -0
  41. package/src/components/DynamicForm/types.ts +83 -1
  42. package/src/components/DynamicForm/utils.ts +32 -0
  43. package/src/components/Popover.tsx +6 -1
  44. package/src/components/ScriptTestPanel.logic.test.ts +139 -0
  45. package/src/components/ScriptTestPanel.logic.ts +137 -0
  46. package/src/components/ScriptTestPanel.tsx +394 -0
  47. package/src/components/Sheet.tsx +21 -6
  48. package/src/components/TemplateInput.tsx +104 -0
  49. package/src/components/TemplateInputToggle.tsx +111 -0
  50. package/src/components/TemplateValueInput.test.ts +98 -0
  51. package/src/components/TemplateValueInput.tsx +470 -0
  52. package/src/components/TimeOfDayInput.tsx +116 -0
  53. package/src/components/VariablePicker.tsx +271 -0
  54. package/src/components/comboboxInteraction.ts +39 -0
  55. package/src/components/portalContainer.ts +24 -0
  56. package/src/hooks/useInitOnceForKey.test.ts +27 -0
  57. package/src/hooks/useInitOnceForKey.ts +21 -18
  58. package/src/index.ts +9 -0
  59. package/stories/ActionCard.stories.tsx +122 -0
  60. package/stories/Alert.stories.tsx +5 -5
  61. package/stories/CodeEditor.stories.tsx +47 -2
  62. package/stories/DurationInput.stories.tsx +59 -0
  63. package/stories/ScriptTestPanel.stories.tsx +106 -0
  64. package/stories/SecretEnvEditor.stories.tsx +80 -0
  65. package/stories/TemplateInputToggle.stories.tsx +77 -0
  66. package/stories/TemplateValueInput.stories.tsx +65 -0
  67. package/stories/TimeOfDayInput.stories.tsx +34 -0
  68. package/stories/VariablePicker.stories.tsx +109 -0
  69. package/tsconfig.json +1 -0
  70. package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
  71. package/src/components/CodeEditor/monacoStdlib.ts +0 -62
  72. package/src/components/CodeEditor/monacoWorkers.ts +0 -118
package/package.json CHANGED
@@ -1,36 +1,44 @@
1
1
  {
2
2
  "name": "@checkstack/ui",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "dependencies": {
8
- "@checkstack/common": "0.10.0",
9
- "@checkstack/frontend-api": "0.5.1",
10
- "@monaco-editor/react": "^4.7.0",
8
+ "@checkstack/common": "0.12.0",
9
+ "@checkstack/frontend-api": "0.6.0",
10
+ "@codingame/monaco-vscode-editor-api": "25.1.2",
11
+ "@codingame/monaco-vscode-languages-service-override": "25.1.2",
12
+ "@codingame/monaco-vscode-standalone-json-language-features": "25.1.2",
13
+ "@codingame/monaco-vscode-standalone-languages": "25.1.2",
14
+ "@codingame/monaco-vscode-standalone-typescript-language-features": "25.1.2",
11
15
  "@radix-ui/react-accordion": "^1.2.12",
12
16
  "@radix-ui/react-dialog": "^1.1.15",
13
17
  "@radix-ui/react-popover": "^1.1.15",
14
18
  "@radix-ui/react-select": "^2.2.6",
15
19
  "@radix-ui/react-slider": "^1.2.1",
16
20
  "@radix-ui/react-slot": "^1.2.4",
21
+ "@typefox/monaco-editor-react": "7.7.0",
17
22
  "ajv": "^8.18.0",
18
23
  "ajv-formats": "^3.0.1",
19
24
  "class-variance-authority": "^0.7.1",
20
25
  "clsx": "^2.1.0",
21
26
  "date-fns": "^4.1.0",
27
+ "fast-xml-parser": "^5.8.0",
28
+ "jsonc-parser": "^3.3.1",
22
29
  "lucide-react": "0.562.0",
23
- "monaco-editor": "^0.55.1",
30
+ "monaco-languageclient": "10.7.0",
24
31
  "react": "^18.2.0",
25
32
  "react-day-picker": "^9.13.0",
26
33
  "react-dom": "^18.2.0",
27
34
  "react-markdown": "^10.1.0",
28
35
  "react-router-dom": "^6.20.0",
29
36
  "recharts": "^3.6.0",
30
- "tailwind-merge": "^2.2.0"
37
+ "tailwind-merge": "^2.2.0",
38
+ "yaml": "^2.9.0"
31
39
  },
32
40
  "devDependencies": {
33
- "@checkstack/scripts": "0.3.2",
41
+ "@checkstack/scripts": "0.3.4",
34
42
  "@checkstack/test-utils-frontend": "0.0.5",
35
43
  "@checkstack/tsconfig": "0.0.7",
36
44
  "@storybook/addon-a11y": "^10.3.6",
@@ -19,12 +19,20 @@
19
19
  *
20
20
  * Run with `bun run generate:monaco-types` from `core/ui`. The output JSON
21
21
  * lives at `src/components/CodeEditor/generated/stdlib-types.json` and is
22
- * lazy-imported by MonacoEditor (so the ~3 MB payload is code-split into
23
- * its own chunk and never blocks initial page load).
22
+ * lazy-imported by the editor (so the ~3 MB payload is code-split into its
23
+ * own chunk and never blocks initial page load).
24
+ *
25
+ * It ALSO emits `generated/builtin-modules.json`: the authoritative list of
26
+ * importable built-in specifiers (`node:fs`, bare `fs`, `bun`, `bun:test`, ...)
27
+ * derived from the SAME bundled declarations (every importable built-in is a
28
+ * top-level `declare module "<spec>"`). The editor ships this so the
29
+ * import-name completion provider can suggest sandbox built-ins regardless of
30
+ * the installed-package allowlist, and it auto-updates with the bundled types.
24
31
  */
25
32
  import { createRequire } from "node:module";
26
33
  import { readdir, readFile, mkdir, writeFile } from "node:fs/promises";
27
34
  import path from "node:path";
35
+ import { extractBuiltinModuleSpecifiers } from "../src/components/CodeEditor/importSpecifiers";
28
36
 
29
37
  const require = createRequire(import.meta.url);
30
38
 
@@ -88,3 +96,18 @@ const totalBytes = Object.values(files).reduce((acc, c) => acc + c.length, 0);
88
96
  console.log(
89
97
  `✅ Wrote ${Object.keys(files).length} files (${(totalBytes / 1024 / 1024).toFixed(2)} MB) → ${path.relative(process.cwd(), outFile)}`,
90
98
  );
99
+
100
+ // Derive the authoritative built-in import specifier list from the SAME
101
+ // bundled declarations: every importable built-in (`node:fs`, bare `fs`,
102
+ // `bun`, `bun:test`, ...) is a top-level `declare module "<spec>"`, so the
103
+ // name set falls out of the d.ts text directly. Wildcard/asset-glob shims
104
+ // (`*.txt`, `*/bun.lock`) are filtered out by the extractor. This auto-updates
105
+ // whenever the bundled `@types/node` / `bun-types` are regenerated, so the
106
+ // editor's import-name completions never drift from the runtime stdlib.
107
+ const allDeclarations = Object.values(files).join("\n");
108
+ const builtinModules = extractBuiltinModuleSpecifiers(allDeclarations);
109
+ const builtinsFile = path.join(outDir, "builtin-modules.json");
110
+ await writeFile(builtinsFile, JSON.stringify(builtinModules), "utf8");
111
+ console.log(
112
+ `✅ Wrote ${builtinModules.length} built-in module specifiers → ${path.relative(process.cwd(), builtinsFile)}`,
113
+ );
@@ -0,0 +1,309 @@
1
+ import React from "react";
2
+ import {
3
+ ChevronDown,
4
+ ChevronRight,
5
+ GripVertical,
6
+ AlertTriangle,
7
+ MoreVertical,
8
+ Trash2,
9
+ } from "lucide-react";
10
+ import { Card, CardContent, CardHeader } from "./Card";
11
+ import { Button } from "./Button";
12
+ import { Toggle } from "./Toggle";
13
+ import { DynamicIcon, type LucideIconName } from "./DynamicIcon";
14
+ import { Badge, type BadgeProps } from "./Badge";
15
+ import { Popover, PopoverContent, PopoverTrigger } from "./Popover";
16
+ import { DropdownMenuItem, MenuCloseContext } from "./Menu";
17
+ import { cn } from "../utils";
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?: LucideIconName;
27
+ onClick: () => void;
28
+ /** `destructive` tints the item red (e.g. Delete). Defaults to neutral. */
29
+ variant?: "default" | "destructive";
30
+ }
31
+
32
+ export interface ActionCardProps {
33
+ /** Stable identifier used for drag-reorder + React key. */
34
+ id: string;
35
+ /** Bold header label, e.g. "Notify User". */
36
+ title: string;
37
+ /** Operator-supplied description (the action's `id`/`description` field). */
38
+ description?: string;
39
+ /** Plugin/category label rendered as a subdued badge. */
40
+ category?: string;
41
+ /** Lucide icon (PascalCase) shown to the left of the title. */
42
+ icon?: LucideIconName;
43
+ /** Toggle for the action's `enabled` flag. Omit to hide the toggle. */
44
+ enabled?: boolean;
45
+ onEnabledChange?: (enabled: boolean) => void;
46
+ /** Removes the card from its container. Omit to hide the delete button. */
47
+ onDelete?: () => void;
48
+ /** Drag handle shown on the left; integrators wire it up via `dnd-kit`. */
49
+ dragHandleProps?: React.HTMLAttributes<HTMLButtonElement>;
50
+ /** Initial expanded state when uncontrolled. */
51
+ defaultExpanded?: boolean;
52
+ /** Controlled expanded state. */
53
+ expanded?: boolean;
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[];
75
+ /** Extra badges (e.g. produces / consumes hints). */
76
+ badges?: Array<{
77
+ label: string;
78
+ variant?: BadgeProps["variant"];
79
+ className?: string;
80
+ }>;
81
+ /** Card body — the action's config form. */
82
+ children?: React.ReactNode;
83
+ /** Optional class override on the outer Card. */
84
+ className?: string;
85
+ /**
86
+ * Validation messages attached to this card. When non-empty the card
87
+ * is marked with a destructive border + warning icon and the messages
88
+ * are listed in the header, so the operator sees *which* card (and
89
+ * field) is wrong without a separate panel.
90
+ */
91
+ errors?: string[];
92
+ }
93
+
94
+ /**
95
+ * Collapsible card that wraps a single automation action in the visual
96
+ * editor.
97
+ *
98
+ * Mirrors the visual layout of `StrategyConfigCard` but stays decoupled
99
+ * from `DynamicForm` so containers (`ChooseBlock`, `ParallelBlock`,
100
+ * `RepeatBlock`) can supply their own children — typically nested lists
101
+ * of `ActionCard`s rather than a flat config schema.
102
+ *
103
+ * Composition pattern (consumed by automation-frontend's editor):
104
+ *
105
+ * ```tsx
106
+ * <ActionCard
107
+ * id={action.id}
108
+ * title={action.action}
109
+ * enabled={action.enabled ?? true}
110
+ * onEnabledChange={(next) => onActionChange({ ...action, enabled: next })}
111
+ * onDelete={() => onDelete(action.id)}
112
+ * >
113
+ * <DynamicForm
114
+ * schema={registeredAction.configJsonSchema}
115
+ * value={action.config}
116
+ * onChange={(config) => onActionChange({ ...action, config })}
117
+ * />
118
+ * </ActionCard>
119
+ * ```
120
+ *
121
+ * The toggle, delete, and drag handle each gate on their respective
122
+ * callback being supplied — pass `onDelete` to render the trash button,
123
+ * `onEnabledChange` to render the enable toggle, `dragHandleProps` to
124
+ * render the grip. Containers that don't want any of those (e.g. a
125
+ * disabled preview) just omit them.
126
+ */
127
+ export const ActionCard: React.FC<ActionCardProps> = ({
128
+ id,
129
+ title,
130
+ description,
131
+ category,
132
+ icon,
133
+ enabled,
134
+ onEnabledChange,
135
+ onDelete,
136
+ dragHandleProps,
137
+ defaultExpanded = true,
138
+ expanded,
139
+ onExpandedChange,
140
+ onOpenSheet,
141
+ summary,
142
+ actions,
143
+ badges,
144
+ children,
145
+ className,
146
+ errors,
147
+ }) => {
148
+ const [internalExpanded, setInternalExpanded] =
149
+ React.useState(defaultExpanded);
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
+ }
160
+ const next = !isExpanded;
161
+ if (expanded === undefined) setInternalExpanded(next);
162
+ onExpandedChange?.(next);
163
+ };
164
+ const hasErrors = errors !== undefined && errors.length > 0;
165
+
166
+ return (
167
+ <Card
168
+ data-action-id={id}
169
+ className={cn(
170
+ "transition-opacity",
171
+ enabled === false && "opacity-60",
172
+ hasErrors && "border-destructive/60 ring-1 ring-destructive/30",
173
+ className,
174
+ )}
175
+ >
176
+ <CardHeader className="flex flex-row items-center gap-2 p-3 space-y-0">
177
+ {dragHandleProps && (
178
+ <button
179
+ type="button"
180
+ className="cursor-grab text-muted-foreground hover:text-foreground"
181
+ aria-label="Drag to reorder"
182
+ {...dragHandleProps}
183
+ >
184
+ <GripVertical className="w-4 h-4" />
185
+ </button>
186
+ )}
187
+ <button
188
+ type="button"
189
+ onClick={handleHeaderClick}
190
+ className="flex items-center flex-1 gap-2 text-left"
191
+ aria-expanded={sheetMode ? undefined : isExpanded}
192
+ aria-controls={sheetMode ? undefined : `${id}-body`}
193
+ >
194
+ {sheetMode ? (
195
+ <ChevronRight className="w-4 h-4 shrink-0 text-muted-foreground" />
196
+ ) : isExpanded ? (
197
+ <ChevronDown className="w-4 h-4 shrink-0 text-muted-foreground" />
198
+ ) : (
199
+ <ChevronRight className="w-4 h-4 shrink-0 text-muted-foreground" />
200
+ )}
201
+ {hasErrors ? (
202
+ <AlertTriangle className="w-4 h-4 shrink-0 text-destructive" />
203
+ ) : (
204
+ icon && <DynamicIcon name={icon} className="w-4 h-4 shrink-0" />
205
+ )}
206
+ <div className="flex-1 min-w-0">
207
+ <div className="flex items-center gap-2">
208
+ <span className="text-sm font-semibold truncate">{title}</span>
209
+ {category && (
210
+ <Badge variant="outline" className="shrink-0 text-[10px]">
211
+ {category}
212
+ </Badge>
213
+ )}
214
+ {badges?.map((badge) => (
215
+ <Badge
216
+ key={badge.label}
217
+ variant={badge.variant ?? "outline"}
218
+ className={cn("shrink-0 text-[10px]", badge.className)}
219
+ >
220
+ {badge.label}
221
+ </Badge>
222
+ ))}
223
+ </div>
224
+ {(summary ?? description) && (
225
+ <p className="text-xs truncate text-muted-foreground">
226
+ {summary ?? description}
227
+ </p>
228
+ )}
229
+ {hasErrors && (
230
+ <ul className="mt-0.5 space-y-0.5">
231
+ {errors!.map((error, index) => (
232
+ <li
233
+ key={index}
234
+ className="text-[11px] font-mono text-destructive"
235
+ >
236
+ {error}
237
+ </li>
238
+ ))}
239
+ </ul>
240
+ )}
241
+ </div>
242
+ </button>
243
+ {onEnabledChange && (
244
+ <Toggle
245
+ checked={enabled ?? true}
246
+ onCheckedChange={onEnabledChange}
247
+ aria-label={enabled ? "Disable action" : "Enable action"}
248
+ />
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
+ )}
289
+ {onDelete && (
290
+ <Button
291
+ type="button"
292
+ variant="ghost"
293
+ size="icon"
294
+ onClick={onDelete}
295
+ className="w-8 h-8 text-destructive hover:text-destructive/90 hover:bg-destructive/10 shrink-0"
296
+ aria-label="Delete action"
297
+ >
298
+ <Trash2 className="w-4 h-4" />
299
+ </Button>
300
+ )}
301
+ </CardHeader>
302
+ {isExpanded && children && (
303
+ <CardContent id={`${id}-body`} className="p-3 border-t border-border">
304
+ {children}
305
+ </CardContent>
306
+ )}
307
+ </Card>
308
+ );
309
+ };
@@ -1,13 +1,136 @@
1
- // Monaco-based CodeEditor
2
- // Re-export all from MonacoEditor as the new CodeEditor implementation
1
+ // Public `CodeEditor` component. Adapts the stable `CodeEditorProps` API to the
2
+ // `@typefox/monaco-editor-react`-backed `TypefoxEditor` (real VS Code language
3
+ // services in the browser). Consumers (DynamicForm, automation, healthcheck)
4
+ // import this and are unaffected by the underlying editor implementation.
5
+ import { useState } from "react";
6
+ import { Maximize2 } from "lucide-react";
7
+ import { TypefoxEditor, type TypefoxEditorProps } from "./TypefoxEditor";
8
+ import { popoutTitle } from "./popoutTitle";
9
+ import type { CodeEditorProps } from "./types";
10
+ import {
11
+ Dialog,
12
+ DialogContent,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ } from "../Dialog";
16
+ import { cn } from "../../utils";
3
17
 
4
- export {
5
- CodeEditor,
6
- type CodeEditorProps,
7
- type CodeEditorLanguage,
8
- type TemplateProperty,
9
- type ShellEnvVar,
10
- } from "./MonacoEditor";
18
+ /**
19
+ * Code editor with context-aware IntelliSense, template / shell completion, and
20
+ * structural validation. See `CodeEditorProps`.
21
+ *
22
+ * Renders the inline editor with a subtle top-right "expand" affordance that
23
+ * opens the SAME editor (same `value` / `onChange` / completion props) in a
24
+ * large full-screen overlay for comfortably editing big scripts. Both the
25
+ * inline and overlay editors are `TypefoxEditor` instances bound to the same
26
+ * controlled `value`, so edits stay in sync and closing the overlay keeps them.
27
+ */
28
+ export const CodeEditor = ({
29
+ id,
30
+ value,
31
+ onChange,
32
+ language = "typescript",
33
+ minHeight = "100px",
34
+ readOnly,
35
+ placeholder,
36
+ typeDefinitions,
37
+ templateProperties,
38
+ shellEnvVars,
39
+ markers,
40
+ acquireTypes,
41
+ acquireResetKey,
42
+ importablePackages,
43
+ allowPopout = true,
44
+ title,
45
+ }: CodeEditorProps) => {
46
+ const [popoutOpen, setPopoutOpen] = useState(false);
47
+
48
+ // CodeEditorProps.minHeight is a CSS length string ("240px"); TypefoxEditor
49
+ // takes a pixel number.
50
+ const minHeightPx = Number.parseInt(minHeight, 10) || 100;
51
+ const editorId = id ?? "code-editor";
52
+
53
+ // Shared editor props for both the inline and overlay instances. The overlay
54
+ // instance overrides only `id` (a distinct model URI so the two Monaco models
55
+ // don't fight) and `fillHeight` (so it fills the tall dialog body). Both are
56
+ // bound to the same `value` / `onChange`, keeping edits in sync.
57
+ const sharedEditorProps: Omit<TypefoxEditorProps, "id" | "fillHeight"> = {
58
+ value,
59
+ onChange,
60
+ language,
61
+ minHeight: minHeightPx,
62
+ readOnly,
63
+ placeholder,
64
+ typeDefinitions,
65
+ templateProperties,
66
+ shellEnvVars,
67
+ markers,
68
+ acquireTypes,
69
+ acquireResetKey,
70
+ importablePackages,
71
+ };
72
+
73
+ const dialogTitle = title ?? popoutTitle({ language });
74
+
75
+ return (
76
+ <div className="relative">
77
+ <TypefoxEditor id={editorId} {...sharedEditorProps} />
78
+ {allowPopout && (
79
+ <button
80
+ type="button"
81
+ aria-label="Expand editor"
82
+ title="Expand editor"
83
+ onClick={() => setPopoutOpen(true)}
84
+ className={cn(
85
+ // Sits above the editor in the top-right corner. A faint background
86
+ // keeps the muted icon legible over code; hover emphasises it.
87
+ "absolute right-2 top-2 z-10 inline-flex h-7 w-7 items-center justify-center",
88
+ "rounded-md bg-background/70 text-muted-foreground backdrop-blur-sm",
89
+ "transition-colors hover:bg-accent hover:text-accent-foreground",
90
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
91
+ )}
92
+ >
93
+ <Maximize2 className="h-4 w-4" />
94
+ </button>
95
+ )}
96
+ <Dialog open={popoutOpen} onOpenChange={setPopoutOpen}>
97
+ {/* Tall flex column: override the default `overflow-y-auto` so the
98
+ editor (not the dialog) scrolls internally, and give the body a
99
+ fixed tall height so the `fillHeight` editor has something to fill. */}
100
+ <DialogContent
101
+ size="full"
102
+ className="flex h-[85dvh] flex-col overflow-hidden overflow-y-hidden"
103
+ >
104
+ <DialogHeader>
105
+ <DialogTitle>{dialogTitle}</DialogTitle>
106
+ </DialogHeader>
107
+ {/* Lazy: the second Monaco instance only mounts while the dialog is
108
+ open, so there's no double-editor cost when closed. Distinct
109
+ `${id}-popout` id => distinct model URI. */}
110
+ {popoutOpen && (
111
+ <div className="flex-1 min-h-0">
112
+ <TypefoxEditor
113
+ id={`${editorId}-popout`}
114
+ fillHeight
115
+ {...sharedEditorProps}
116
+ />
117
+ </div>
118
+ )}
119
+ </DialogContent>
120
+ </Dialog>
121
+ </div>
122
+ );
123
+ };
124
+
125
+ export type {
126
+ CodeEditorProps,
127
+ CodeEditorLanguage,
128
+ TemplateProperty,
129
+ ShellEnvVar,
130
+ EditorMarker,
131
+ AcquireTypes,
132
+ AcquiredTypeFile,
133
+ } from "./types";
11
134
 
12
135
  export {
13
136
  generateTypeDefinitions,