@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.
- package/.storybook/main.ts +43 -0
- package/CHANGELOG.md +565 -0
- package/package.json +15 -7
- package/scripts/generate-stdlib-types.ts +25 -2
- package/src/components/ActionCard.tsx +309 -0
- package/src/components/CodeEditor/CodeEditor.tsx +132 -9
- package/src/components/CodeEditor/TypefoxEditor.tsx +1024 -0
- package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
- package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
- package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
- 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/monacoTsService.ts +217 -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 +41 -0
- package/src/components/CodeEditor/scriptContext.ts +76 -1
- package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
- package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
- package/src/components/CodeEditor/templateValidation.ts +51 -0
- package/src/components/CodeEditor/types.ts +168 -0
- package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
- package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
- package/src/components/CodeEditor/validateScripts.ts +132 -0
- package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
- package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
- package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
- package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
- package/src/components/Dialog.tsx +32 -11
- package/src/components/DurationInput.tsx +121 -0
- package/src/components/DynamicForm/DynamicForm.tsx +27 -1
- package/src/components/DynamicForm/FormField.tsx +138 -10
- package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +83 -9
- package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
- package/src/components/DynamicForm/index.ts +6 -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 +83 -1
- package/src/components/DynamicForm/utils.ts +32 -0
- 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/TemplateInput.tsx +104 -0
- package/src/components/TemplateInputToggle.tsx +111 -0
- package/src/components/TemplateValueInput.test.ts +98 -0
- package/src/components/TemplateValueInput.tsx +470 -0
- package/src/components/TimeOfDayInput.tsx +116 -0
- package/src/components/VariablePicker.tsx +271 -0
- package/src/components/comboboxInteraction.ts +39 -0
- package/src/components/portalContainer.ts +24 -0
- package/src/hooks/useInitOnceForKey.test.ts +27 -0
- package/src/hooks/useInitOnceForKey.ts +21 -18
- package/src/index.ts +9 -0
- package/stories/ActionCard.stories.tsx +122 -0
- package/stories/Alert.stories.tsx +5 -5
- package/stories/CodeEditor.stories.tsx +47 -2
- package/stories/DurationInput.stories.tsx +59 -0
- package/stories/ScriptTestPanel.stories.tsx +106 -0
- package/stories/SecretEnvEditor.stories.tsx +80 -0
- package/stories/TemplateInputToggle.stories.tsx +77 -0
- package/stories/TemplateValueInput.stories.tsx +65 -0
- package/stories/TimeOfDayInput.stories.tsx +34 -0
- package/stories/VariablePicker.stories.tsx +109 -0
- package/tsconfig.json +1 -0
- package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
- package/src/components/CodeEditor/monacoStdlib.ts +0 -62
- 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.
|
|
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.
|
|
9
|
-
"@checkstack/frontend-api": "0.
|
|
10
|
-
"@monaco-editor
|
|
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-
|
|
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.
|
|
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
|
|
23
|
-
*
|
|
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
|
-
//
|
|
2
|
-
//
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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,
|