@checkstack/ui 0.2.4 → 0.3.1

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 (33) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/package.json +10 -3
  3. package/src/components/CodeEditor/CodeEditor.tsx +420 -0
  4. package/src/components/CodeEditor/index.ts +10 -0
  5. package/src/components/CodeEditor/languageSupport/enterBehavior.test.ts +173 -0
  6. package/src/components/CodeEditor/languageSupport/enterBehavior.ts +35 -0
  7. package/src/components/CodeEditor/languageSupport/index.ts +22 -0
  8. package/src/components/CodeEditor/languageSupport/json-utils.ts +117 -0
  9. package/src/components/CodeEditor/languageSupport/json.test.ts +274 -0
  10. package/src/components/CodeEditor/languageSupport/json.ts +139 -0
  11. package/src/components/CodeEditor/languageSupport/markdown-utils.ts +65 -0
  12. package/src/components/CodeEditor/languageSupport/markdown.test.ts +245 -0
  13. package/src/components/CodeEditor/languageSupport/markdown.ts +134 -0
  14. package/src/components/CodeEditor/languageSupport/types.ts +48 -0
  15. package/src/components/CodeEditor/languageSupport/xml-utils.ts +94 -0
  16. package/src/components/CodeEditor/languageSupport/xml.test.ts +239 -0
  17. package/src/components/CodeEditor/languageSupport/xml.ts +116 -0
  18. package/src/components/CodeEditor/languageSupport/yaml-utils.ts +101 -0
  19. package/src/components/CodeEditor/languageSupport/yaml.test.ts +203 -0
  20. package/src/components/CodeEditor/languageSupport/yaml.ts +120 -0
  21. package/src/components/DynamicForm/DynamicForm.tsx +2 -24
  22. package/src/components/DynamicForm/DynamicOptionsField.tsx +48 -21
  23. package/src/components/DynamicForm/FormField.tsx +38 -70
  24. package/src/components/DynamicForm/JsonField.tsx +19 -25
  25. package/src/components/DynamicForm/KeyValueEditor.tsx +314 -0
  26. package/src/components/DynamicForm/MultiTypeEditorField.tsx +465 -0
  27. package/src/components/DynamicForm/index.ts +13 -0
  28. package/src/components/DynamicForm/types.ts +14 -8
  29. package/src/components/DynamicForm/utils.test.ts +390 -0
  30. package/src/components/DynamicForm/utils.ts +142 -3
  31. package/src/index.ts +1 -1
  32. package/src/components/TemplateEditor.test.ts +0 -156
  33. package/src/components/TemplateEditor.tsx +0 -435
@@ -0,0 +1,120 @@
1
+ import { yaml } from "@codemirror/lang-yaml";
2
+ import { Decoration } from "@codemirror/view";
3
+ import type { LanguageSupport, DecorationRange } from "./types";
4
+
5
+ // Re-export pure utils for backwards compatibility
6
+ export {
7
+ isValidYamlTemplatePosition,
8
+ calculateYamlIndentation,
9
+ } from "./yaml-utils";
10
+
11
+ // Decoration marks for YAML syntax highlighting using inline styles
12
+ const yamlKeyMark = Decoration.mark({
13
+ attributes: { style: "color: hsl(210, 100%, 75%)" }, // Bright blue for better visibility
14
+ });
15
+ const yamlStringMark = Decoration.mark({
16
+ attributes: { style: "color: hsl(142.1, 76.2%, 36.3%)" },
17
+ });
18
+ const yamlNumberMark = Decoration.mark({
19
+ attributes: { style: "color: hsl(217.2, 91.2%, 59.8%)" },
20
+ });
21
+ const yamlBoolMark = Decoration.mark({
22
+ attributes: { style: "color: hsl(280, 65%, 60%)" },
23
+ });
24
+ const templateMark = Decoration.mark({
25
+ attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
26
+ });
27
+
28
+ /**
29
+ * Build YAML + template decorations.
30
+ */
31
+ function buildYamlDecorations(doc: string): DecorationRange[] {
32
+ const ranges: DecorationRange[] = [];
33
+
34
+ // Match templates first (highest priority)
35
+ const templateRegex = /\{\{[\w.[\]]*\}\}/g;
36
+ let match;
37
+ while ((match = templateRegex.exec(doc)) !== null) {
38
+ ranges.push({
39
+ from: match.index,
40
+ to: match.index + match[0].length,
41
+ decoration: templateMark,
42
+ });
43
+ }
44
+
45
+ // Match YAML keys (word followed by colon)
46
+ const keyRegex = /^(\s*)([\w-]+):/gm;
47
+ while ((match = keyRegex.exec(doc)) !== null) {
48
+ const keyStart = match.index + match[1].length;
49
+ const keyEnd = keyStart + match[2].length;
50
+ ranges.push({
51
+ from: keyStart,
52
+ to: keyEnd,
53
+ decoration: yamlKeyMark,
54
+ });
55
+ }
56
+
57
+ // Match quoted strings
58
+ const stringRegex = /(["'])(?:(?!\1)[^\\]|\\.)*\1/g;
59
+ while ((match = stringRegex.exec(doc)) !== null) {
60
+ ranges.push({
61
+ from: match.index,
62
+ to: match.index + match[0].length,
63
+ decoration: yamlStringMark,
64
+ });
65
+ }
66
+
67
+ // Match numbers (standalone)
68
+ const numberRegex = /(?<=:\s+)-?\d+\.?\d*(?:\s|$)/g;
69
+ while ((match = numberRegex.exec(doc)) !== null) {
70
+ ranges.push({
71
+ from: match.index,
72
+ to: match.index + match[0].trim().length,
73
+ decoration: yamlNumberMark,
74
+ });
75
+ }
76
+
77
+ // Match booleans
78
+ const boolRegex = /(?<=:\s+)(true|false|yes|no|on|off)(?:\s|$)/gi;
79
+ while ((match = boolRegex.exec(doc)) !== null) {
80
+ ranges.push({
81
+ from: match.index,
82
+ to: match.index + match[1].length,
83
+ decoration: yamlBoolMark,
84
+ });
85
+ }
86
+
87
+ // Sort by position
88
+ ranges.sort((a, b) => a.from - b.from || a.to - b.to);
89
+
90
+ // Remove overlaps (templates take priority)
91
+ const filtered: DecorationRange[] = [];
92
+ for (const range of ranges) {
93
+ const overlaps = filtered.some(
94
+ (existing) =>
95
+ (range.from >= existing.from && range.from < existing.to) ||
96
+ (range.to > existing.from && range.to <= existing.to),
97
+ );
98
+ if (!overlaps) {
99
+ filtered.push(range);
100
+ }
101
+ }
102
+
103
+ return filtered;
104
+ }
105
+
106
+ // Import the pure utils for use in the language support object
107
+ import {
108
+ isValidYamlTemplatePosition,
109
+ calculateYamlIndentation,
110
+ } from "./yaml-utils";
111
+
112
+ /**
113
+ * YAML language support for CodeEditor with template expression handling.
114
+ */
115
+ export const yamlLanguageSupport: LanguageSupport = {
116
+ extension: yaml(),
117
+ buildDecorations: buildYamlDecorations,
118
+ isValidTemplatePosition: isValidYamlTemplatePosition,
119
+ calculateIndentation: calculateYamlIndentation,
120
+ };
@@ -2,32 +2,10 @@ import React from "react";
2
2
 
3
3
  import { EmptyState } from "../../index";
4
4
 
5
- import type { DynamicFormProps, JsonSchemaProperty } from "./types";
6
- import { extractDefaults } from "./utils";
5
+ import type { DynamicFormProps } from "./types";
6
+ import { extractDefaults, isValueEmpty } from "./utils";
7
7
  import { FormField } from "./FormField";
8
8
 
9
- /**
10
- * Check if a value is considered "empty" for validation purposes.
11
- */
12
- function isValueEmpty(val: unknown, propSchema: JsonSchemaProperty): boolean {
13
- if (val === undefined || val === null) return true;
14
- if (typeof val === "string" && val.trim() === "") return true;
15
- // For arrays, check if empty
16
- if (Array.isArray(val) && val.length === 0) return true;
17
- // For objects (nested schemas), recursively check required fields
18
- if (propSchema.type === "object" && propSchema.properties) {
19
- const objVal = val as Record<string, unknown>;
20
- const requiredKeys = propSchema.required ?? [];
21
- for (const key of requiredKeys) {
22
- const nestedPropSchema = propSchema.properties[key];
23
- if (nestedPropSchema && isValueEmpty(objVal[key], nestedPropSchema)) {
24
- return true;
25
- }
26
- }
27
- }
28
- return false;
29
- }
30
-
31
9
  /**
32
10
  * DynamicForm generates a form from a JSON Schema.
33
11
  * Supports all standard JSON Schema types plus custom extensions for
@@ -12,7 +12,7 @@ import {
12
12
  } from "../../index";
13
13
 
14
14
  import type { DynamicOptionsFieldProps, ResolverOption } from "./types";
15
- import { getCleanDescription } from "./utils";
15
+ import { getCleanDescription, NONE_SENTINEL } from "./utils";
16
16
 
17
17
  /**
18
18
  * Field component for dynamically resolved options.
@@ -72,7 +72,7 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
72
72
  .catch((error_) => {
73
73
  if (!cancelled) {
74
74
  setError(
75
- error_ instanceof Error ? error_.message : "Failed to load options"
75
+ error_ instanceof Error ? error_.message : "Failed to load options",
76
76
  );
77
77
  setLoading(false);
78
78
  }
@@ -140,24 +140,41 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
140
140
  No matching options
141
141
  </div>
142
142
  ) : (
143
- filteredOptions.map((opt) => (
144
- <button
145
- key={opt.value}
146
- type="button"
147
- onClick={() => {
148
- onChange(opt.value);
149
- setOpen(false);
150
- setSearchQuery("");
151
- }}
152
- className={`w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground ${
153
- opt.value === value
154
- ? "bg-accent text-accent-foreground"
155
- : ""
156
- }`}
157
- >
158
- {opt.label}
159
- </button>
160
- ))
143
+ <>
144
+ {!isRequired && (
145
+ <button
146
+ type="button"
147
+ onClick={() => {
148
+ onChange();
149
+ setOpen(false);
150
+ setSearchQuery("");
151
+ }}
152
+ className={`w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground text-muted-foreground ${
153
+ value ? "" : "bg-accent text-accent-foreground"
154
+ }`}
155
+ >
156
+ None
157
+ </button>
158
+ )}
159
+ {filteredOptions.map((opt) => (
160
+ <button
161
+ key={opt.value}
162
+ type="button"
163
+ onClick={() => {
164
+ onChange(opt.value);
165
+ setOpen(false);
166
+ setSearchQuery("");
167
+ }}
168
+ className={`w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground ${
169
+ opt.value === value
170
+ ? "bg-accent text-accent-foreground"
171
+ : ""
172
+ }`}
173
+ >
174
+ {opt.label}
175
+ </button>
176
+ ))}
177
+ </>
161
178
  )}
162
179
  </div>
163
180
  </div>
@@ -193,7 +210,9 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
193
210
  ) : (
194
211
  <Select
195
212
  value={(value as string) || ""}
196
- onValueChange={(val) => onChange(val)}
213
+ onValueChange={(val) =>
214
+ onChange(val === NONE_SENTINEL ? undefined : val)
215
+ }
197
216
  disabled={options.length === 0}
198
217
  >
199
218
  <SelectTrigger id={id}>
@@ -206,6 +225,14 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
206
225
  />
207
226
  </SelectTrigger>
208
227
  <SelectContent>
228
+ {!isRequired && (
229
+ <SelectItem
230
+ value={NONE_SENTINEL}
231
+ className="text-muted-foreground"
232
+ >
233
+ None
234
+ </SelectItem>
235
+ )}
209
236
  {options.map((opt) => (
210
237
  <SelectItem key={opt.value} value={opt.value}>
211
238
  {opt.label}
@@ -13,13 +13,13 @@ import {
13
13
  Textarea,
14
14
  Toggle,
15
15
  ColorPicker,
16
- TemplateEditor,
17
16
  } from "../../index";
18
17
 
19
18
  import type { FormFieldProps, JsonSchemaProperty } from "./types";
20
- import { getCleanDescription } from "./utils";
19
+ import { getCleanDescription, NONE_SENTINEL } from "./utils";
21
20
  import { DynamicOptionsField } from "./DynamicOptionsField";
22
21
  import { JsonField } from "./JsonField";
22
+ import { MultiTypeEditorField } from "./MultiTypeEditorField";
23
23
 
24
24
  /**
25
25
  * Recursive field renderer that handles all supported JSON Schema types.
@@ -84,12 +84,22 @@ export const FormField: React.FC<FormFieldProps> = ({
84
84
  <div className="relative">
85
85
  <Select
86
86
  value={(value as string) || (propSchema.default as string) || ""}
87
- onValueChange={(val) => onChange(val)}
87
+ onValueChange={(val) =>
88
+ onChange(val === NONE_SENTINEL ? undefined : val)
89
+ }
88
90
  >
89
91
  <SelectTrigger id={id}>
90
92
  <SelectValue placeholder={`Select ${label}`} />
91
93
  </SelectTrigger>
92
94
  <SelectContent>
95
+ {!isRequired && (
96
+ <SelectItem
97
+ value={NONE_SENTINEL}
98
+ className="text-muted-foreground"
99
+ >
100
+ None
101
+ </SelectItem>
102
+ )}
93
103
  {propSchema.enum.map((opt: string) => (
94
104
  <SelectItem key={opt} value={opt}>
95
105
  {opt}
@@ -104,46 +114,34 @@ export const FormField: React.FC<FormFieldProps> = ({
104
114
 
105
115
  // String
106
116
  if (propSchema.type === "string") {
117
+ const cleanDesc = getCleanDescription(description);
118
+
119
+ // Multi-type editor field (x-editor-types)
120
+ const editorTypes = propSchema["x-editor-types"];
121
+ if (editorTypes && editorTypes.length > 0) {
122
+ return (
123
+ <MultiTypeEditorField
124
+ id={id}
125
+ label={label}
126
+ description={cleanDesc}
127
+ value={value as string | undefined}
128
+ isRequired={isRequired}
129
+ editorTypes={editorTypes}
130
+ templateProperties={templateProperties}
131
+ onChange={onChange as (val: string | undefined) => void}
132
+ />
133
+ );
134
+ }
135
+
107
136
  const isTextarea =
108
137
  propSchema.format === "textarea" ||
109
138
  propSchema.description?.includes("[textarea]");
110
139
  const isSecret = (
111
140
  propSchema as JsonSchemaProperty & { "x-secret"?: boolean }
112
141
  )["x-secret"];
113
- const cleanDesc = getCleanDescription(description);
114
142
 
115
- // Textarea fields - use TemplateEditor if templateProperties available
143
+ // Textarea fields
116
144
  if (isTextarea) {
117
- // If we have template properties, use TemplateEditor
118
- if (templateProperties && templateProperties.length > 0) {
119
- return (
120
- <div className="space-y-2">
121
- <div>
122
- <Label htmlFor={id}>
123
- {label} {isRequired && "*"}
124
- </Label>
125
- {cleanDesc && (
126
- <p className="text-sm text-muted-foreground mt-0.5">
127
- {cleanDesc}
128
- </p>
129
- )}
130
- </div>
131
- <TemplateEditor
132
- value={(value as string) || ""}
133
- onChange={(val) => onChange(val)}
134
- availableProperties={templateProperties}
135
- placeholder={
136
- propSchema.default
137
- ? `Default: ${String(propSchema.default)}`
138
- : "Enter template..."
139
- }
140
- rows={5}
141
- />
142
- </div>
143
- );
144
- }
145
-
146
- // No template properties, fall back to regular textarea
147
145
  return (
148
146
  <div className="space-y-2">
149
147
  <div>
@@ -213,37 +211,7 @@ export const FormField: React.FC<FormFieldProps> = ({
213
211
  );
214
212
  }
215
213
 
216
- // Default string input - use TemplateEditor if templateProperties available
217
- // If we have template properties, use TemplateEditor with smaller rows
218
- if (templateProperties && templateProperties.length > 0) {
219
- return (
220
- <div className="space-y-2">
221
- <div>
222
- <Label htmlFor={id}>
223
- {label} {isRequired && "*"}
224
- </Label>
225
- {cleanDesc && (
226
- <p className="text-sm text-muted-foreground mt-0.5">
227
- {cleanDesc}
228
- </p>
229
- )}
230
- </div>
231
- <TemplateEditor
232
- value={(value as string) || ""}
233
- onChange={(val) => onChange(val)}
234
- availableProperties={templateProperties}
235
- placeholder={
236
- propSchema.default
237
- ? `Default: ${String(propSchema.default)}`
238
- : undefined
239
- }
240
- rows={2}
241
- />
242
- </div>
243
- );
244
- }
245
-
246
- // No template properties - fallback to regular Input
214
+ // Default string input
247
215
  return (
248
216
  <div className="space-y-2">
249
217
  <div>
@@ -291,7 +259,7 @@ export const FormField: React.FC<FormFieldProps> = ({
291
259
  onChange(
292
260
  propSchema.type === "integer"
293
261
  ? Number.parseInt(e.target.value, 10)
294
- : Number.parseFloat(e.target.value)
262
+ : Number.parseFloat(e.target.value),
295
263
  )
296
264
  }
297
265
  />
@@ -390,7 +358,7 @@ export const FormField: React.FC<FormFieldProps> = ({
390
358
  const newItem: Record<string, unknown> = {};
391
359
  // Find discriminator and set all properties with defaults
392
360
  for (const [propKey, propDef] of Object.entries(
393
- firstVariant.properties
361
+ firstVariant.properties,
394
362
  )) {
395
363
  if (propDef.const !== undefined) {
396
364
  // This is the discriminator field
@@ -489,7 +457,7 @@ export const FormField: React.FC<FormFieldProps> = ({
489
457
  // Find discriminator: the field that has "const" in each variant
490
458
  let discriminatorField: string | undefined;
491
459
  for (const [fieldName, fieldSchema] of Object.entries(
492
- firstVariant.properties
460
+ firstVariant.properties,
493
461
  )) {
494
462
  if (fieldSchema.const !== undefined) {
495
463
  discriminatorField = fieldName;
@@ -547,7 +515,7 @@ export const FormField: React.FC<FormFieldProps> = ({
547
515
  };
548
516
  // Set defaults for other properties
549
517
  for (const [propKey, propDef] of Object.entries(
550
- newVariant.properties || {}
518
+ newVariant.properties || {},
551
519
  )) {
552
520
  if (
553
521
  propKey !== discriminatorField &&
@@ -1,11 +1,8 @@
1
1
  import React from "react";
2
2
  import Ajv from "ajv";
3
3
  import addFormats from "ajv-formats";
4
- import Editor from "react-simple-code-editor";
5
- // @ts-expect-error - prismjs doesn't have types for deep imports in some environments
6
- import { highlight, languages } from "prismjs/components/prism-core";
7
- import "prismjs/components/prism-json";
8
4
 
5
+ import { CodeEditor } from "../CodeEditor";
9
6
  import type { JsonFieldProps } from "./types";
10
7
 
11
8
  const ajv = new Ajv({ allErrors: true, strict: false });
@@ -21,7 +18,7 @@ export const JsonField: React.FC<JsonFieldProps> = ({
21
18
  onChange,
22
19
  }) => {
23
20
  const [internalValue, setInternalValue] = React.useState(
24
- JSON.stringify(value || {}, undefined, 2)
21
+ JSON.stringify(value || {}, undefined, 2),
25
22
  );
26
23
  const [error, setError] = React.useState<string | undefined>();
27
24
  const lastPropValue = React.useRef(value);
@@ -59,7 +56,7 @@ export const JsonField: React.FC<JsonFieldProps> = ({
59
56
  setError(
60
57
  validateFn.errors
61
58
  ?.map((e) => `${e.instancePath} ${e.message}`)
62
- .join(", ")
59
+ .join(", "),
63
60
  );
64
61
  return false;
65
62
  }
@@ -71,27 +68,24 @@ export const JsonField: React.FC<JsonFieldProps> = ({
71
68
  }
72
69
  };
73
70
 
71
+ const handleChange = (code: string) => {
72
+ setInternalValue(code);
73
+ const parsed = validate(code);
74
+ if (parsed !== false) {
75
+ lastPropValue.current = parsed;
76
+ onChange(parsed);
77
+ }
78
+ };
79
+
74
80
  return (
75
81
  <div className="space-y-2">
76
- <div className="min-h-[100px] w-full rounded-md border border-input bg-background font-mono text-sm focus-within:ring-2 focus-within:ring-ring focus-within:border-transparent transition-all overflow-hidden box-border">
77
- <Editor
78
- id={id}
79
- value={internalValue}
80
- onValueChange={(code) => {
81
- setInternalValue(code);
82
- const parsed = validate(code);
83
- if (parsed !== false) {
84
- lastPropValue.current = parsed;
85
- onChange(parsed);
86
- }
87
- }}
88
- highlight={(code) => highlight(code, languages.json)}
89
- padding={10}
90
- style={{
91
- minHeight: "100px",
92
- }}
93
- />
94
- </div>
82
+ <CodeEditor
83
+ id={id}
84
+ value={internalValue}
85
+ onChange={handleChange}
86
+ language="json"
87
+ minHeight="100px"
88
+ />
95
89
  {error && <p className="text-xs text-destructive font-medium">{error}</p>}
96
90
  </div>
97
91
  );