@checkstack/ui 1.9.0 → 1.11.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 (52) hide show
  1. package/CHANGELOG.md +417 -0
  2. package/package.json +15 -7
  3. package/scripts/generate-stdlib-types.ts +2 -2
  4. package/src/components/ActionCard.tsx +221 -0
  5. package/src/components/CodeEditor/CodeEditor.tsx +51 -9
  6. package/src/components/CodeEditor/TypefoxEditor.tsx +868 -0
  7. package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
  8. package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
  9. package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
  10. package/src/components/CodeEditor/index.ts +2 -0
  11. package/src/components/CodeEditor/scriptContext.test.ts +41 -0
  12. package/src/components/CodeEditor/scriptContext.ts +76 -1
  13. package/src/components/CodeEditor/templateValidation.ts +51 -0
  14. package/src/components/CodeEditor/types.ts +109 -0
  15. package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
  16. package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
  17. package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
  18. package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
  19. package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
  20. package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
  21. package/src/components/DynamicForm/DynamicForm.tsx +2 -0
  22. package/src/components/DynamicForm/FormField.tsx +29 -9
  23. package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
  24. package/src/components/DynamicForm/MultiTypeEditorField.tsx +16 -7
  25. package/src/components/DynamicForm/types.ts +11 -0
  26. package/src/components/ListEmptyState.tsx +51 -0
  27. package/src/components/QueryErrorState.tsx +64 -0
  28. package/src/components/ResponsiveTable.tsx +92 -0
  29. package/src/components/Skeleton.tsx +39 -0
  30. package/src/components/TemplateInput.tsx +104 -0
  31. package/src/components/TemplateInputToggle.tsx +111 -0
  32. package/src/components/TemplateValueInput.test.ts +98 -0
  33. package/src/components/TemplateValueInput.tsx +470 -0
  34. package/src/components/VariablePicker.tsx +271 -0
  35. package/src/hooks/useInitOnceForKey.test.ts +27 -0
  36. package/src/hooks/useInitOnceForKey.ts +21 -18
  37. package/src/index.ts +10 -0
  38. package/src/utils/toastTemplates.test.ts +82 -0
  39. package/src/utils/toastTemplates.ts +47 -0
  40. package/stories/ActionCard.stories.tsx +62 -0
  41. package/stories/Alert.stories.tsx +5 -5
  42. package/stories/ListEmptyState.stories.tsx +48 -0
  43. package/stories/QueryErrorState.stories.tsx +40 -0
  44. package/stories/ResponsiveTable.stories.tsx +93 -0
  45. package/stories/Skeleton.stories.tsx +53 -0
  46. package/stories/TemplateInputToggle.stories.tsx +77 -0
  47. package/stories/TemplateValueInput.stories.tsx +65 -0
  48. package/stories/VariablePicker.stories.tsx +109 -0
  49. package/stories/toastTemplates.stories.tsx +60 -0
  50. package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
  51. package/src/components/CodeEditor/monacoStdlib.ts +0 -62
  52. package/src/components/CodeEditor/monacoWorkers.ts +0 -118
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { validateJsonTemplate } from "./validateJsonTemplate";
3
+
4
+ describe("validateJsonTemplate", () => {
5
+ it("accepts valid JSON", () => {
6
+ expect(validateJsonTemplate('{"a": 1, "b": "two"}')).toEqual([]);
7
+ });
8
+
9
+ it("accepts empty content", () => {
10
+ expect(validateJsonTemplate("")).toEqual([]);
11
+ expect(validateJsonTemplate(" \n ")).toEqual([]);
12
+ });
13
+
14
+ it("accepts a template as a quoted string value", () => {
15
+ expect(
16
+ validateJsonTemplate('{"repo": "{{trigger.payload.repository}}"}'),
17
+ ).toEqual([]);
18
+ });
19
+
20
+ it("accepts a template embedded inside a string", () => {
21
+ expect(validateJsonTemplate('{"msg": "build {{id}} done"}')).toEqual([]);
22
+ });
23
+
24
+ it("accepts an UNQUOTED template value (e.g. a number)", () => {
25
+ // This is the case a plain JSON validator would reject.
26
+ expect(
27
+ validateJsonTemplate('{"timeout": {{trigger.payload.timeout}}}'),
28
+ ).toEqual([]);
29
+ });
30
+
31
+ it("accepts a template as an array element", () => {
32
+ expect(validateJsonTemplate('{"items": [{{count}}, 2]}')).toEqual([]);
33
+ });
34
+
35
+ it("flags a genuine structural error (missing comma)", () => {
36
+ const diagnostics = validateJsonTemplate('{"a": 1 "b": 2}');
37
+ expect(diagnostics.length).toBeGreaterThan(0);
38
+ });
39
+
40
+ it("flags an unclosed object", () => {
41
+ const diagnostics = validateJsonTemplate('{"a": 1');
42
+ expect(diagnostics.length).toBeGreaterThan(0);
43
+ });
44
+
45
+ it("flags a genuine error even when templates are present, at the right offset", () => {
46
+ // The `2` is missing its preceding colon; offset must point into the
47
+ // ORIGINAL text (same-length substitution preserves offsets).
48
+ const text = '{"a": {{x}}, "b" 2}';
49
+ const diagnostics = validateJsonTemplate(text);
50
+ expect(diagnostics.length).toBeGreaterThan(0);
51
+ // The error should land at/after the `2`, not inside the template.
52
+ const firstOffset = diagnostics[0]?.offset ?? -1;
53
+ expect(text.slice(firstOffset, firstOffset + 1)).toBe("2");
54
+ });
55
+
56
+ it("does not let an unclosed `{{` swallow a later template", () => {
57
+ // `{{` on its own is not a complete template, so substitution leaves it;
58
+ // the real `{{y}}` value is still substituted and the JSON stays valid.
59
+ expect(validateJsonTemplate('{"a": "{{", "b": {{y}}}')).toEqual([]);
60
+ });
61
+ });
@@ -0,0 +1,26 @@
1
+ // Template-aware JSON validation. See templateValidation.ts for the approach.
2
+ // The real VS Code JSON service still provides highlighting + completion; only
3
+ // its built-in (raw-text) validation is turned off in favour of this, so `{{ }}`
4
+ // is tolerated in any position (including unquoted, e.g. a numeric value).
5
+ import { parse, printParseErrorCode, type ParseError } from "jsonc-parser";
6
+ import {
7
+ substituteTemplates,
8
+ type TemplateDiagnostic,
9
+ } from "./templateValidation";
10
+
11
+ /** Validate template-bearing JSON. Empty content is allowed (no diagnostics). */
12
+ export const validateJsonTemplate = (
13
+ text: string,
14
+ ): TemplateDiagnostic[] => {
15
+ const errors: ParseError[] = [];
16
+ parse(substituteTemplates(text), errors, {
17
+ allowEmptyContent: true,
18
+ allowTrailingComma: false,
19
+ disallowComments: true,
20
+ });
21
+ return errors.map((parseError) => ({
22
+ offset: parseError.offset,
23
+ length: Math.max(parseError.length, 1),
24
+ message: `JSON: ${printParseErrorCode(parseError.error)}`,
25
+ }));
26
+ };
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { validateXmlTemplate } from "./validateXmlTemplate";
3
+
4
+ describe("validateXmlTemplate", () => {
5
+ it("accepts valid XML", () => {
6
+ expect(validateXmlTemplate("<root><a>1</a></root>")).toEqual([]);
7
+ });
8
+
9
+ it("accepts empty content", () => {
10
+ expect(validateXmlTemplate("")).toEqual([]);
11
+ expect(validateXmlTemplate(" \n ")).toEqual([]);
12
+ });
13
+
14
+ it("accepts a template in element text", () => {
15
+ expect(
16
+ validateXmlTemplate("<root><repo>{{trigger.payload.repository}}</repo></root>"),
17
+ ).toEqual([]);
18
+ });
19
+
20
+ it("accepts a template in a (quoted) attribute value", () => {
21
+ expect(validateXmlTemplate('<root attr="{{id}}"></root>')).toEqual([]);
22
+ });
23
+
24
+ it("flags a mismatched closing tag", () => {
25
+ const diagnostics = validateXmlTemplate("<a><b></a>");
26
+ expect(diagnostics.length).toBeGreaterThan(0);
27
+ expect(diagnostics[0]?.message).toContain("XML:");
28
+ });
29
+
30
+ it("flags an unclosed tag even with templates present", () => {
31
+ const diagnostics = validateXmlTemplate("<root><x>{{v}}</root>");
32
+ expect(diagnostics.length).toBeGreaterThan(0);
33
+ });
34
+ });
@@ -0,0 +1,35 @@
1
+ // Template-aware XML validation. See templateValidation.ts for the approach.
2
+ // There is no @codingame standalone XML language service (only Monarch
3
+ // highlighting), so we use fast-xml-parser's validator after template
4
+ // substitution. The validator reports the FIRST error only, with 1-based
5
+ // line/column, which we convert to an offset/range in the original text.
6
+ import { XMLValidator } from "fast-xml-parser";
7
+ import {
8
+ lineColumnToOffset,
9
+ substituteTemplates,
10
+ type TemplateDiagnostic,
11
+ } from "./templateValidation";
12
+
13
+ /** Validate template-bearing XML. Empty content is allowed (no diagnostics). */
14
+ export const validateXmlTemplate = (
15
+ text: string,
16
+ ): TemplateDiagnostic[] => {
17
+ const substituted = substituteTemplates(text);
18
+ if (substituted.trim() === "") {
19
+ return [];
20
+ }
21
+ const result = XMLValidator.validate(substituted, {
22
+ allowBooleanAttributes: true,
23
+ });
24
+ if (result === true) {
25
+ return [];
26
+ }
27
+ const { msg, line, col } = result.err;
28
+ return [
29
+ {
30
+ offset: lineColumnToOffset({ text: substituted, line, column: col }),
31
+ length: 1,
32
+ message: `XML: ${msg}`,
33
+ },
34
+ ];
35
+ };
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { validateYamlTemplate } from "./validateYamlTemplate";
3
+
4
+ describe("validateYamlTemplate", () => {
5
+ it("accepts valid YAML", () => {
6
+ expect(validateYamlTemplate("a: 1\nb: two\n")).toEqual([]);
7
+ });
8
+
9
+ it("accepts empty content", () => {
10
+ expect(validateYamlTemplate("")).toEqual([]);
11
+ });
12
+
13
+ it("accepts a template as a quoted string value", () => {
14
+ expect(validateYamlTemplate('repo: "{{trigger.payload.repository}}"')).toEqual(
15
+ [],
16
+ );
17
+ });
18
+
19
+ it("accepts an UNQUOTED template value (e.g. a number)", () => {
20
+ expect(validateYamlTemplate("timeout: {{trigger.payload.timeout}}")).toEqual(
21
+ [],
22
+ );
23
+ });
24
+
25
+ it("accepts a template as a list item", () => {
26
+ expect(validateYamlTemplate("items:\n - {{count}}\n - 2")).toEqual([]);
27
+ });
28
+
29
+ it("flags tabs used as indentation", () => {
30
+ const diagnostics = validateYamlTemplate("foo:\n\tbar: 1");
31
+ expect(diagnostics.length).toBeGreaterThan(0);
32
+ expect(diagnostics[0]?.message).toContain("YAML:");
33
+ });
34
+
35
+ it("flags a genuine structural error (with templates present)", () => {
36
+ const diagnostics = validateYamlTemplate("a: {{x}}\n b: 2");
37
+ expect(diagnostics.length).toBeGreaterThan(0);
38
+ });
39
+ });
@@ -0,0 +1,28 @@
1
+ // Template-aware YAML validation. See templateValidation.ts for the approach.
2
+ // There is no @codingame standalone YAML language service (only Monarch
3
+ // highlighting), so we parse with the `yaml` package after template
4
+ // substitution. YAML is permissive (it often parses `{{ }}` as a flow
5
+ // collection), but substitution gives accurate structural validation and still
6
+ // catches real mistakes (bad indentation, tabs, duplicate keys, ...).
7
+ import { parseDocument } from "yaml";
8
+ import {
9
+ substituteTemplates,
10
+ type TemplateDiagnostic,
11
+ } from "./templateValidation";
12
+
13
+ /** Validate template-bearing YAML. */
14
+ export const validateYamlTemplate = (
15
+ text: string,
16
+ ): TemplateDiagnostic[] => {
17
+ const doc = parseDocument(substituteTemplates(text), {
18
+ prettyErrors: false,
19
+ });
20
+ return doc.errors.map((error) => {
21
+ const [start, end] = error.pos;
22
+ return {
23
+ offset: start,
24
+ length: Math.max(end - start, 1),
25
+ message: `YAML: ${error.message}`,
26
+ };
27
+ });
28
+ };
@@ -18,6 +18,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
18
18
  onValidChange,
19
19
  optionsResolvers,
20
20
  templateProperties,
21
+ templateCompletionProvider,
21
22
  typeDefinitions,
22
23
  shellEnvVars,
23
24
  starterTemplates,
@@ -116,6 +117,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
116
117
  formValues={value}
117
118
  optionsResolvers={optionsResolvers}
118
119
  templateProperties={templateProperties}
120
+ templateCompletionProvider={templateCompletionProvider}
119
121
  typeDefinitions={typeDefinitions}
120
122
  shellEnvVars={shellEnvVars}
121
123
  starterTemplates={starterTemplates}
@@ -13,6 +13,7 @@ import {
13
13
  Textarea,
14
14
  Toggle,
15
15
  ColorPicker,
16
+ TemplateValueInput,
16
17
  } from "../../index";
17
18
 
18
19
  import type { FormFieldProps, JsonSchemaProperty } from "./types";
@@ -33,6 +34,7 @@ export const FormField: React.FC<FormFieldProps> = ({
33
34
  formValues,
34
35
  optionsResolvers,
35
36
  templateProperties,
37
+ templateCompletionProvider,
36
38
  typeDefinitions,
37
39
  shellEnvVars,
38
40
  starterTemplates,
@@ -232,7 +234,14 @@ export const FormField: React.FC<FormFieldProps> = ({
232
234
  );
233
235
  }
234
236
 
235
- // Default string input
237
+ // Default string input. When a completion provider is supplied the
238
+ // field is templatable (e.g. automation action config), so render a
239
+ // TemplateValueInput wired to it for `{{ … }}` autocomplete; without
240
+ // one, keep the bare Input so other DynamicForm consumers are
241
+ // unaffected.
242
+ const placeholder = propSchema.default
243
+ ? `Default: ${String(propSchema.default)}`
244
+ : "";
236
245
  return (
237
246
  <div className="space-y-2">
238
247
  <div>
@@ -243,14 +252,22 @@ export const FormField: React.FC<FormFieldProps> = ({
243
252
  <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
244
253
  )}
245
254
  </div>
246
- <Input
247
- id={id}
248
- value={(value as string) || ""}
249
- onChange={(e) => onChange(e.target.value)}
250
- placeholder={
251
- propSchema.default ? `Default: ${String(propSchema.default)}` : ""
252
- }
253
- />
255
+ {templateCompletionProvider ? (
256
+ <TemplateValueInput
257
+ id={id}
258
+ value={(value as string) || ""}
259
+ onChange={(next) => onChange(next)}
260
+ placeholder={placeholder}
261
+ completionProvider={templateCompletionProvider}
262
+ />
263
+ ) : (
264
+ <Input
265
+ id={id}
266
+ value={(value as string) || ""}
267
+ onChange={(e) => onChange(e.target.value)}
268
+ placeholder={placeholder}
269
+ />
270
+ )}
254
271
  </div>
255
272
  );
256
273
  }
@@ -352,6 +369,7 @@ export const FormField: React.FC<FormFieldProps> = ({
352
369
  formValues={formValues}
353
370
  optionsResolvers={optionsResolvers}
354
371
  templateProperties={templateProperties}
372
+ templateCompletionProvider={templateCompletionProvider}
355
373
  typeDefinitions={typeDefinitions}
356
374
  shellEnvVars={shellEnvVars}
357
375
  starterTemplates={starterTemplates}
@@ -463,6 +481,7 @@ export const FormField: React.FC<FormFieldProps> = ({
463
481
  formValues={formValues}
464
482
  optionsResolvers={optionsResolvers}
465
483
  templateProperties={templateProperties}
484
+ templateCompletionProvider={templateCompletionProvider}
466
485
  typeDefinitions={typeDefinitions}
467
486
  shellEnvVars={shellEnvVars}
468
487
  starterTemplates={starterTemplates}
@@ -594,6 +613,7 @@ export const FormField: React.FC<FormFieldProps> = ({
594
613
  formValues={formValues}
595
614
  optionsResolvers={optionsResolvers}
596
615
  templateProperties={templateProperties}
616
+ templateCompletionProvider={templateCompletionProvider}
597
617
  typeDefinitions={typeDefinitions}
598
618
  shellEnvVars={shellEnvVars}
599
619
  starterTemplates={starterTemplates}
@@ -3,6 +3,7 @@ import { Plus, Trash2 } from "lucide-react";
3
3
  import { Button } from "../Button";
4
4
  import { Input } from "../Input";
5
5
  import type { TemplateProperty } from "../CodeEditor";
6
+ import { TemplateValueInput } from "../TemplateValueInput";
6
7
 
7
8
  export interface KeyValuePair {
8
9
  key: string;
@@ -28,24 +29,6 @@ export interface KeyValueEditorProps {
28
29
  templateProperties?: TemplateProperty[];
29
30
  }
30
31
 
31
- /**
32
- * Detect if cursor is inside an unclosed {{ template context.
33
- */
34
- function detectTemplateContext(text: string, cursorPos: number) {
35
- const textBefore = text.slice(0, cursorPos);
36
- const lastOpenBrace = textBefore.lastIndexOf("{{");
37
- const lastCloseBrace = textBefore.lastIndexOf("}}");
38
-
39
- if (lastOpenBrace !== -1 && lastOpenBrace > lastCloseBrace) {
40
- return {
41
- isInTemplate: true,
42
- query: textBefore.slice(lastOpenBrace + 2),
43
- startPos: lastOpenBrace,
44
- };
45
- }
46
- return { isInTemplate: false, query: "", startPos: -1 };
47
- }
48
-
49
32
  /**
50
33
  * A key/value pair editor for form data and similar use cases.
51
34
  * Supports adding/removing pairs and optional template autocomplete in values.
@@ -131,7 +114,7 @@ export const KeyValueEditor: React.FC<KeyValueEditorProps> = ({
131
114
  className="flex-1 font-mono text-sm"
132
115
  />
133
116
  <span className="text-muted-foreground">=</span>
134
- <TemplateInput
117
+ <TemplateValueInput
135
118
  id={`${id}-value-${index}`}
136
119
  value={pair.value}
137
120
  onChange={(newValue) => handleValueChange(index, newValue)}
@@ -162,153 +145,3 @@ export const KeyValueEditor: React.FC<KeyValueEditorProps> = ({
162
145
  </div>
163
146
  );
164
147
  };
165
-
166
- /**
167
- * An input with simple template autocomplete support.
168
- * Shows a dropdown when user types "{{".
169
- */
170
- const TemplateInput: React.FC<{
171
- id: string;
172
- value: string;
173
- onChange: (value: string) => void;
174
- placeholder?: string;
175
- templateProperties?: TemplateProperty[];
176
- }> = ({ id, value, onChange, placeholder, templateProperties }) => {
177
- const inputRef = React.useRef<HTMLInputElement>(null);
178
- const [showPopup, setShowPopup] = React.useState(false);
179
- const [selectedIndex, setSelectedIndex] = React.useState(0);
180
- const [templateContext, setTemplateContext] = React.useState<{
181
- query: string;
182
- startPos: number;
183
- }>({ query: "", startPos: -1 });
184
-
185
- // Filter properties based on query
186
- const filteredProperties = React.useMemo(() => {
187
- if (!templateProperties) return [];
188
- if (!templateContext.query.trim()) return templateProperties;
189
- const lowerQuery = templateContext.query.toLowerCase();
190
- return templateProperties.filter((prop) =>
191
- prop.path.toLowerCase().includes(lowerQuery),
192
- );
193
- }, [templateProperties, templateContext.query]);
194
-
195
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
196
- const newValue = e.target.value;
197
- onChange(newValue);
198
-
199
- if (!templateProperties || templateProperties.length === 0) return;
200
-
201
- const cursorPos = e.target.selectionStart ?? newValue.length;
202
- const context = detectTemplateContext(newValue, cursorPos);
203
-
204
- if (context.isInTemplate) {
205
- setTemplateContext({ query: context.query, startPos: context.startPos });
206
- setShowPopup(true);
207
- setSelectedIndex(0);
208
- } else {
209
- setShowPopup(false);
210
- }
211
- };
212
-
213
- const insertProperty = (prop: TemplateProperty) => {
214
- if (templateContext.startPos === -1) return;
215
-
216
- const cursorPos = inputRef.current?.selectionStart ?? value.length;
217
- const template = `{{${prop.path}}}`;
218
- const newValue =
219
- value.slice(0, templateContext.startPos) +
220
- template +
221
- value.slice(cursorPos);
222
-
223
- onChange(newValue);
224
- setShowPopup(false);
225
-
226
- // Restore focus
227
- setTimeout(() => {
228
- inputRef.current?.focus();
229
- const newPos = templateContext.startPos + template.length;
230
- inputRef.current?.setSelectionRange(newPos, newPos);
231
- }, 0);
232
- };
233
-
234
- const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
235
- if (!showPopup || filteredProperties.length === 0) return;
236
-
237
- switch (e.key) {
238
- case "ArrowDown": {
239
- e.preventDefault();
240
- setSelectedIndex((prev) =>
241
- prev < filteredProperties.length - 1 ? prev + 1 : 0,
242
- );
243
- break;
244
- }
245
- case "ArrowUp": {
246
- e.preventDefault();
247
- setSelectedIndex((prev) =>
248
- prev > 0 ? prev - 1 : filteredProperties.length - 1,
249
- );
250
- break;
251
- }
252
- case "Enter":
253
- case "Tab": {
254
- e.preventDefault();
255
- if (filteredProperties[selectedIndex]) {
256
- insertProperty(filteredProperties[selectedIndex]);
257
- }
258
- break;
259
- }
260
- case "Escape": {
261
- e.preventDefault();
262
- setShowPopup(false);
263
- break;
264
- }
265
- }
266
- };
267
-
268
- // Close popup on blur
269
- const handleBlur = () => {
270
- // Delay to allow click on popup item
271
- setTimeout(() => setShowPopup(false), 150);
272
- };
273
-
274
- return (
275
- <div className="relative flex-1">
276
- <Input
277
- ref={inputRef}
278
- id={id}
279
- value={value}
280
- onChange={handleChange}
281
- onKeyDown={handleKeyDown}
282
- onBlur={handleBlur}
283
- placeholder={placeholder}
284
- className="font-mono text-sm"
285
- />
286
- {showPopup && filteredProperties.length > 0 && (
287
- <div className="absolute z-50 top-full left-0 mt-1 w-64 max-h-48 overflow-y-auto rounded-md border border-border bg-popover shadow-lg">
288
- <div className="p-1">
289
- {filteredProperties.map((prop, index) => (
290
- <button
291
- key={prop.path}
292
- type="button"
293
- onMouseDown={(e) => {
294
- e.preventDefault();
295
- insertProperty(prop);
296
- }}
297
- className={`w-full flex items-center justify-between gap-2 px-2 py-1.5 text-xs rounded-sm text-left hover:bg-accent hover:text-accent-foreground ${
298
- index === selectedIndex
299
- ? "bg-accent text-accent-foreground"
300
- : ""
301
- }`}
302
- >
303
- <code className="font-mono truncate">{prop.path}</code>
304
- <span className="text-muted-foreground shrink-0">
305
- {prop.type}
306
- </span>
307
- </button>
308
- ))}
309
- </div>
310
- </div>
311
- )}
312
- </div>
313
- );
314
- };
@@ -336,6 +336,14 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
336
336
  />
337
337
  )}
338
338
 
339
+ {/*
340
+ Native-code editors (javascript / typescript / shell) intentionally
341
+ do NOT receive `templateProperties`: `{{ }}` template syntax is for
342
+ text/markup fields only. Code fields access run context through
343
+ their language's native mechanism instead — a typed `context`
344
+ object (driven by `typeDefinitions`) for TS/JS, and `$`-prefixed
345
+ env vars (driven by `shellEnvVars`) for shell.
346
+ */}
339
347
  {selectedType === "javascript" && (
340
348
  <CodeEditor
341
349
  id={id}
@@ -343,7 +351,6 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
343
351
  onChange={onChange}
344
352
  language="javascript"
345
353
  minHeight="150px"
346
- templateProperties={templateProperties}
347
354
  typeDefinitions={typeDefinitions}
348
355
  />
349
356
  )}
@@ -355,7 +362,6 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
355
362
  onChange={onChange}
356
363
  language="typescript"
357
364
  minHeight="150px"
358
- templateProperties={templateProperties}
359
365
  typeDefinitions={typeDefinitions}
360
366
  />
361
367
  )}
@@ -367,7 +373,6 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
367
373
  onChange={onChange}
368
374
  language="shell"
369
375
  minHeight="150px"
370
- templateProperties={templateProperties}
371
376
  shellEnvVars={shellEnvVars}
372
377
  />
373
378
  )}
@@ -417,8 +422,10 @@ const RawEditor: React.FC<{
417
422
  if (!templateProperties) return [];
418
423
  if (!templateContext.query.trim()) return templateProperties;
419
424
  const lowerQuery = templateContext.query.toLowerCase();
420
- return templateProperties.filter((prop) =>
421
- prop.path.toLowerCase().includes(lowerQuery),
425
+ return templateProperties.filter(
426
+ (prop) =>
427
+ prop.path.toLowerCase().includes(lowerQuery) ||
428
+ (prop.templateRef?.toLowerCase().includes(lowerQuery) ?? false),
422
429
  );
423
430
  }, [templateProperties, templateContext.query]);
424
431
 
@@ -481,7 +488,7 @@ const RawEditor: React.FC<{
481
488
  const textarea = textareaRef.current;
482
489
  if (!textarea || templateContext.startPos === -1) return;
483
490
 
484
- const template = `{{${prop.path}}}`;
491
+ const template = `{{${prop.templateRef ?? prop.path}}}`;
485
492
  const cursorPos = textarea.selectionStart ?? 0;
486
493
  const newValue =
487
494
  value.slice(0, templateContext.startPos) +
@@ -580,7 +587,9 @@ const RawEditor: React.FC<{
580
587
  : ""
581
588
  }`}
582
589
  >
583
- <code className="font-mono truncate">{prop.path}</code>
590
+ <code className="font-mono truncate">
591
+ {prop.templateRef ?? prop.path}
592
+ </code>
584
593
  <span className="text-muted-foreground shrink-0">
585
594
  {prop.type}
586
595
  </span>
@@ -1,4 +1,5 @@
1
1
  import type { TemplateProperty, ShellEnvVar } from "../CodeEditor";
2
+ import type { TemplateCompletionProvider } from "../TemplateValueInput";
2
3
  import type { EditorType } from "@checkstack/common";
3
4
 
4
5
  // Re-export types used by multi-type editor
@@ -69,6 +70,15 @@ export interface DynamicFormProps {
69
70
  * When provided, fields with x-editor-types get {{ autocomplete suggestions.
70
71
  */
71
72
  templateProperties?: TemplateProperty[];
73
+ /**
74
+ * Optional staged, context-aware completion provider for plain
75
+ * single-line string fields. When supplied, default string inputs
76
+ * render a {@link TemplateValueInput} wired to this provider instead
77
+ * of a bare `Input`, so `{{ … }}` expressions get field / comparator /
78
+ * value / filter autocomplete (the automation editor passes the
79
+ * template-mode provider here). Omit it and string fields stay plain.
80
+ */
81
+ templateCompletionProvider?: TemplateCompletionProvider;
72
82
  /**
73
83
  * Optional TypeScript declarations to inject into Monaco for `typescript`
74
84
  * or `javascript` editor-type fields. Typically built from a schema via
@@ -100,6 +110,7 @@ export interface FormFieldProps {
100
110
  formValues: Record<string, unknown>;
101
111
  optionsResolvers?: Record<string, OptionsResolver>;
102
112
  templateProperties?: TemplateProperty[];
113
+ templateCompletionProvider?: TemplateCompletionProvider;
103
114
  typeDefinitions?: string;
104
115
  shellEnvVars?: ShellEnvVar[];
105
116
  starterTemplates?: EditorStarterTemplates;
@@ -0,0 +1,51 @@
1
+ import React from "react";
2
+ import { Inbox } from "lucide-react";
3
+ import { EmptyState } from "./EmptyState";
4
+
5
+ interface ListEmptyStateProps {
6
+ /**
7
+ * The name of the resource type the list would display (e.g. `"checks"`,
8
+ * `"incidents"`). Drives the default `"No {resource} yet"` headline.
9
+ */
10
+ resource: string;
11
+ /**
12
+ * Optional supplemental description rendered beneath the headline.
13
+ */
14
+ description?: React.ReactNode;
15
+ /**
16
+ * Optional action area, typically a primary CTA button such as
17
+ * "Create your first check".
18
+ */
19
+ actions?: React.ReactNode;
20
+ /**
21
+ * Optional icon override. Defaults to the lucide `Inbox` glyph so callers
22
+ * don't have to pick one for every list.
23
+ */
24
+ icon?: React.ReactNode;
25
+ }
26
+
27
+ /**
28
+ * ListEmptyState - the canonical empty state for a list-shaped resource.
29
+ *
30
+ * Thin wrapper around {@link EmptyState} that supplies a consistent
31
+ * "No {resource} yet" headline and a sensible default icon. Use this on
32
+ * any page that renders a list and may have zero items so the UX stays
33
+ * uniform across plugins.
34
+ */
35
+ export const ListEmptyState: React.FC<ListEmptyStateProps> = ({
36
+ resource,
37
+ description,
38
+ actions,
39
+ icon,
40
+ }) => {
41
+ const resolvedIcon = icon ?? <Inbox className="h-10 w-10" />;
42
+
43
+ return (
44
+ <EmptyState
45
+ title={`No ${resource} yet`}
46
+ description={description}
47
+ icon={resolvedIcon}
48
+ actions={actions}
49
+ />
50
+ );
51
+ };