@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,245 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import {
4
+ isValidMarkdownTemplatePosition,
5
+ calculateMarkdownIndentation,
6
+ } from "./markdown-utils";
7
+
8
+ describe("isValidMarkdownTemplatePosition", () => {
9
+ describe("valid text positions", () => {
10
+ it("returns true for plain text", () => {
11
+ expect(isValidMarkdownTemplatePosition("Hello world")).toBe(true);
12
+ });
13
+
14
+ it("returns true after heading", () => {
15
+ expect(isValidMarkdownTemplatePosition("# Title\n")).toBe(true);
16
+ });
17
+
18
+ it("returns true in paragraph", () => {
19
+ expect(isValidMarkdownTemplatePosition("Some text here")).toBe(true);
20
+ });
21
+
22
+ it("returns true after bold text", () => {
23
+ expect(isValidMarkdownTemplatePosition("**bold** and more")).toBe(true);
24
+ });
25
+
26
+ it("returns true after italic text", () => {
27
+ expect(isValidMarkdownTemplatePosition("*italic* text")).toBe(true);
28
+ });
29
+
30
+ it("returns true in list item", () => {
31
+ expect(isValidMarkdownTemplatePosition("- item text")).toBe(true);
32
+ });
33
+
34
+ it("returns true in numbered list item", () => {
35
+ expect(isValidMarkdownTemplatePosition("1. first item")).toBe(true);
36
+ });
37
+
38
+ it("returns true in blockquote", () => {
39
+ expect(isValidMarkdownTemplatePosition("> quote text")).toBe(true);
40
+ });
41
+ });
42
+
43
+ describe("code positions (invalid)", () => {
44
+ it("returns false inside code block", () => {
45
+ expect(isValidMarkdownTemplatePosition("```\ncode here")).toBe(false);
46
+ });
47
+
48
+ it("returns false inside inline code", () => {
49
+ expect(isValidMarkdownTemplatePosition("Here is `some code")).toBe(false);
50
+ });
51
+
52
+ it("returns true after closed code block", () => {
53
+ expect(isValidMarkdownTemplatePosition("```\ncode\n```\n")).toBe(true);
54
+ });
55
+
56
+ it("returns true after closed inline code", () => {
57
+ expect(isValidMarkdownTemplatePosition("Here is `code` and more")).toBe(
58
+ true,
59
+ );
60
+ });
61
+
62
+ it("returns false in code block with language", () => {
63
+ expect(isValidMarkdownTemplatePosition("```javascript\nvar x")).toBe(
64
+ false,
65
+ );
66
+ });
67
+
68
+ it("returns true after code block with content", () => {
69
+ expect(isValidMarkdownTemplatePosition("```js\ncode\n```\ntext")).toBe(
70
+ true,
71
+ );
72
+ });
73
+ });
74
+
75
+ describe("edge cases", () => {
76
+ it("returns true for empty line after heading", () => {
77
+ expect(isValidMarkdownTemplatePosition("# Heading\n\n")).toBe(true);
78
+ });
79
+
80
+ it("returns true for link text", () => {
81
+ expect(isValidMarkdownTemplatePosition("[link text")).toBe(true);
82
+ });
83
+
84
+ it("returns true after link", () => {
85
+ expect(isValidMarkdownTemplatePosition("[text](url) more")).toBe(true);
86
+ });
87
+
88
+ it("returns false for multiple unclosed code blocks", () => {
89
+ expect(isValidMarkdownTemplatePosition("```\n```\n```\ncode")).toBe(
90
+ false,
91
+ );
92
+ });
93
+
94
+ it("returns true for multiple closed code blocks", () => {
95
+ expect(isValidMarkdownTemplatePosition("```\n```\n```\n```\ntext")).toBe(
96
+ true,
97
+ );
98
+ });
99
+ });
100
+ });
101
+
102
+ describe("calculateMarkdownIndentation", () => {
103
+ const INDENT = 2;
104
+
105
+ // ============================================================================
106
+ // Basic indentation
107
+ // ============================================================================
108
+ describe("basic indentation", () => {
109
+ it("returns 0 for empty string", () => {
110
+ expect(calculateMarkdownIndentation("", INDENT)).toBe(0);
111
+ });
112
+
113
+ it("returns 0 for simple text", () => {
114
+ expect(calculateMarkdownIndentation("Hello world", INDENT)).toBe(0);
115
+ });
116
+
117
+ it("returns 0 after heading", () => {
118
+ expect(calculateMarkdownIndentation("# Heading\n", INDENT)).toBe(0);
119
+ });
120
+
121
+ it("returns 0 after paragraph", () => {
122
+ expect(calculateMarkdownIndentation("Some text.\n", INDENT)).toBe(0);
123
+ });
124
+ });
125
+
126
+ // ============================================================================
127
+ // List indentation
128
+ // ============================================================================
129
+ describe("list indentation", () => {
130
+ it("maintains list indentation after list item", () => {
131
+ expect(calculateMarkdownIndentation("- item\n", INDENT)).toBe(0);
132
+ });
133
+
134
+ it("maintains nested list indentation", () => {
135
+ expect(calculateMarkdownIndentation(" - nested item\n", INDENT)).toBe(2);
136
+ });
137
+
138
+ it("handles numbered list", () => {
139
+ expect(calculateMarkdownIndentation("1. item\n", INDENT)).toBe(0);
140
+ });
141
+
142
+ it("handles deeply nested list", () => {
143
+ const text = `- item
144
+ - nested
145
+ - deep\n`;
146
+ expect(calculateMarkdownIndentation(text, INDENT)).toBe(4);
147
+ });
148
+ });
149
+
150
+ // ============================================================================
151
+ // Template handling
152
+ // ============================================================================
153
+ describe("template handling", () => {
154
+ it("handles templates in text", () => {
155
+ expect(calculateMarkdownIndentation("Hello {{name}}", INDENT)).toBe(0);
156
+ });
157
+
158
+ it("handles templates in list items", () => {
159
+ expect(calculateMarkdownIndentation("- {{item}}\n", INDENT)).toBe(0);
160
+ });
161
+
162
+ it("handles templates in headings", () => {
163
+ expect(calculateMarkdownIndentation("# {{title}}\n", INDENT)).toBe(0);
164
+ });
165
+ });
166
+
167
+ // ============================================================================
168
+ // Code block indentation
169
+ // ============================================================================
170
+ describe("code block indentation", () => {
171
+ it("returns 0 after code block start", () => {
172
+ expect(calculateMarkdownIndentation("```\n", INDENT)).toBe(0);
173
+ });
174
+
175
+ it("returns 0 inside code block", () => {
176
+ expect(calculateMarkdownIndentation("```\ncode line\n", INDENT)).toBe(0);
177
+ });
178
+
179
+ it("returns 0 after code block end", () => {
180
+ expect(calculateMarkdownIndentation("```\ncode\n```\n", INDENT)).toBe(0);
181
+ });
182
+ });
183
+
184
+ // ============================================================================
185
+ // Edge cases
186
+ // ============================================================================
187
+ describe("edge cases", () => {
188
+ it("handles only newline", () => {
189
+ expect(calculateMarkdownIndentation("\n", INDENT)).toBe(0);
190
+ });
191
+
192
+ it("handles multiple newlines", () => {
193
+ expect(calculateMarkdownIndentation("\n\n\n", INDENT)).toBe(0);
194
+ });
195
+
196
+ it("handles blockquote", () => {
197
+ expect(calculateMarkdownIndentation("> quote\n", INDENT)).toBe(0);
198
+ });
199
+
200
+ it("handles horizontal rule", () => {
201
+ expect(calculateMarkdownIndentation("---\n", INDENT)).toBe(0);
202
+ });
203
+
204
+ it("handles table row", () => {
205
+ expect(calculateMarkdownIndentation("| col1 | col2 |\n", INDENT)).toBe(0);
206
+ });
207
+ });
208
+
209
+ // ============================================================================
210
+ // Real-world document structures
211
+ // ============================================================================
212
+ describe("real-world document structures", () => {
213
+ it("handles README-style document", () => {
214
+ const text = `# Project
215
+
216
+ Some description.
217
+
218
+ ## Features
219
+
220
+ - Feature 1
221
+ - Feature 2\n`;
222
+ expect(calculateMarkdownIndentation(text, INDENT)).toBe(0);
223
+ });
224
+
225
+ it("handles nested list structure", () => {
226
+ const text = `## Tasks
227
+
228
+ - Task 1
229
+ - Subtask A
230
+ - Subtask B\n`;
231
+ expect(calculateMarkdownIndentation(text, INDENT)).toBe(2);
232
+ });
233
+
234
+ it("handles code example in docs", () => {
235
+ const text = `## Usage
236
+
237
+ \`\`\`javascript
238
+ const x = 1;
239
+ \`\`\`
240
+
241
+ More text\n`;
242
+ expect(calculateMarkdownIndentation(text, INDENT)).toBe(0);
243
+ });
244
+ });
245
+ });
@@ -0,0 +1,134 @@
1
+ import { markdown } from "@codemirror/lang-markdown";
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
+ isValidMarkdownTemplatePosition,
8
+ calculateMarkdownIndentation,
9
+ } from "./markdown-utils";
10
+
11
+ // Decoration marks for Markdown syntax highlighting using inline styles
12
+ const mdHeadingMark = Decoration.mark({
13
+ attributes: { style: "color: hsl(280, 65%, 60%); font-weight: bold" },
14
+ });
15
+ const mdBoldMark = Decoration.mark({
16
+ attributes: { style: "font-weight: bold" },
17
+ });
18
+ const mdItalicMark = Decoration.mark({
19
+ attributes: { style: "font-style: italic" },
20
+ });
21
+ const mdCodeMark = Decoration.mark({
22
+ attributes: {
23
+ style:
24
+ "color: hsl(142.1, 76.2%, 36.3%); background: hsla(0, 0%, 50%, 0.1); border-radius: 3px; padding: 0 2px",
25
+ },
26
+ });
27
+ const mdLinkMark = Decoration.mark({
28
+ attributes: { style: "color: hsl(217.2, 91.2%, 59.8%)" },
29
+ });
30
+ const templateMark = Decoration.mark({
31
+ attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
32
+ });
33
+
34
+ /**
35
+ * Build Markdown + template decorations.
36
+ */
37
+ function buildMarkdownDecorations(doc: string): DecorationRange[] {
38
+ const ranges: DecorationRange[] = [];
39
+
40
+ // Match templates first (highest priority)
41
+ const templateRegex = /\{\{[\w.[\]]*\}\}/g;
42
+ let match;
43
+ while ((match = templateRegex.exec(doc)) !== null) {
44
+ ranges.push({
45
+ from: match.index,
46
+ to: match.index + match[0].length,
47
+ decoration: templateMark,
48
+ });
49
+ }
50
+
51
+ // Match headings (# ... or ## ... etc.)
52
+ const headingRegex = /^(#{1,6})\s+.+$/gm;
53
+ while ((match = headingRegex.exec(doc)) !== null) {
54
+ ranges.push({
55
+ from: match.index,
56
+ to: match.index + match[0].length,
57
+ decoration: mdHeadingMark,
58
+ });
59
+ }
60
+
61
+ // Match bold (**text** or __text__)
62
+ const boldRegex = /(\*\*|__)(?!\s)(.+?)(?<!\s)\1/g;
63
+ while ((match = boldRegex.exec(doc)) !== null) {
64
+ ranges.push({
65
+ from: match.index,
66
+ to: match.index + match[0].length,
67
+ decoration: mdBoldMark,
68
+ });
69
+ }
70
+
71
+ // Match italic (*text* or _text_) - but not **
72
+ const italicRegex = /(?<!\*|\w)(\*|_)(?!\*|\s)(.+?)(?<!\s|\*)\1(?!\*|\w)/g;
73
+ while ((match = italicRegex.exec(doc)) !== null) {
74
+ ranges.push({
75
+ from: match.index,
76
+ to: match.index + match[0].length,
77
+ decoration: mdItalicMark,
78
+ });
79
+ }
80
+
81
+ // Match inline code (`code`)
82
+ const codeRegex = /`[^`\n]+`/g;
83
+ while ((match = codeRegex.exec(doc)) !== null) {
84
+ ranges.push({
85
+ from: match.index,
86
+ to: match.index + match[0].length,
87
+ decoration: mdCodeMark,
88
+ });
89
+ }
90
+
91
+ // Match links [text](url)
92
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
93
+ while ((match = linkRegex.exec(doc)) !== null) {
94
+ ranges.push({
95
+ from: match.index,
96
+ to: match.index + match[0].length,
97
+ decoration: mdLinkMark,
98
+ });
99
+ }
100
+
101
+ // Sort by position
102
+ ranges.sort((a, b) => a.from - b.from || a.to - b.to);
103
+
104
+ // Remove overlaps (templates take priority)
105
+ const filtered: DecorationRange[] = [];
106
+ for (const range of ranges) {
107
+ const overlaps = filtered.some(
108
+ (existing) =>
109
+ (range.from >= existing.from && range.from < existing.to) ||
110
+ (range.to > existing.from && range.to <= existing.to),
111
+ );
112
+ if (!overlaps) {
113
+ filtered.push(range);
114
+ }
115
+ }
116
+
117
+ return filtered;
118
+ }
119
+
120
+ // Import the pure utils for use in the language support object
121
+ import {
122
+ isValidMarkdownTemplatePosition,
123
+ calculateMarkdownIndentation,
124
+ } from "./markdown-utils";
125
+
126
+ /**
127
+ * Markdown language support for CodeEditor with template expression handling.
128
+ */
129
+ export const markdownLanguageSupport: LanguageSupport = {
130
+ extension: markdown(),
131
+ buildDecorations: buildMarkdownDecorations,
132
+ isValidTemplatePosition: isValidMarkdownTemplatePosition,
133
+ calculateIndentation: calculateMarkdownIndentation,
134
+ };
@@ -0,0 +1,48 @@
1
+ import type { Extension } from "@codemirror/state";
2
+ import type { Decoration } from "@codemirror/view";
3
+
4
+ /**
5
+ * A decoration range to be applied to the document.
6
+ */
7
+ export interface DecorationRange {
8
+ from: number;
9
+ to: number;
10
+ decoration: Decoration;
11
+ }
12
+
13
+ /**
14
+ * Interface for language-specific support in CodeEditor.
15
+ * Each language (JSON, YAML, etc.) implements this interface to provide
16
+ * syntax highlighting that works correctly with {{template}} expressions.
17
+ */
18
+ export interface LanguageSupport {
19
+ /**
20
+ * The CodeMirror language extension for this language.
21
+ * Provides indentation, bracket matching, folding, etc.
22
+ */
23
+ extension: Extension;
24
+
25
+ /**
26
+ * Build decoration ranges for syntax highlighting.
27
+ * This is used to override the Lezer parser's highlighting when templates
28
+ * are present (since {{}} confuses most language parsers).
29
+ * @param doc The document text
30
+ * @returns Array of decoration ranges to apply
31
+ */
32
+ buildDecorations: (doc: string) => DecorationRange[];
33
+
34
+ /**
35
+ * Check if the cursor position is valid for template insertion.
36
+ * @param textBefore The document text before the cursor
37
+ * @returns true if templates can be inserted at this position
38
+ */
39
+ isValidTemplatePosition: (textBefore: string) => boolean;
40
+
41
+ /**
42
+ * Calculate the indentation level for a given line.
43
+ * This is used to provide correct indentation that won't be confused by templates.
44
+ * @param textBefore All text before the current line
45
+ * @returns Number of spaces to indent
46
+ */
47
+ calculateIndentation: (textBefore: string, indentUnit: number) => number;
48
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Pure utility functions for XML parsing that don't depend on CodeMirror.
3
+ * These are extracted to allow testing without triggering CodeMirror's module loading.
4
+ */
5
+
6
+ /**
7
+ * Check if cursor is in a valid XML template position.
8
+ * Templates can appear in:
9
+ * 1. Text content between tags: <tag>|</tag>
10
+ * 2. Attribute values: <tag attr="|">
11
+ * @internal Exported for testing
12
+ */
13
+ export function isValidXmlTemplatePosition(text: string): boolean {
14
+ // Check if we're inside an attribute value (inside quotes after =)
15
+ let inTag = false;
16
+ let inAttrValue = false;
17
+ let quoteChar = "";
18
+
19
+ for (let i = 0; i < text.length; i++) {
20
+ const char = text[i];
21
+
22
+ if (!inTag && char === "<" && text[i + 1] !== "/") {
23
+ inTag = true;
24
+ inAttrValue = false;
25
+ } else if (inTag && char === ">") {
26
+ inTag = false;
27
+ inAttrValue = false;
28
+ } else if (inTag && !inAttrValue && char === "=") {
29
+ // Next should be a quote
30
+ const nextChar = text[i + 1];
31
+ if (nextChar === '"' || nextChar === "'") {
32
+ quoteChar = nextChar;
33
+ inAttrValue = true;
34
+ i++; // Skip the quote
35
+ }
36
+ } else if (inAttrValue && char === quoteChar) {
37
+ inAttrValue = false;
38
+ quoteChar = "";
39
+ }
40
+ }
41
+
42
+ // Valid if inside attribute value
43
+ if (inAttrValue) {
44
+ return true;
45
+ }
46
+
47
+ // Valid if between tags (text content position)
48
+ // Check if we're after a closing > and not inside an opening <
49
+ const lastOpenTag = text.lastIndexOf("<");
50
+ const lastCloseTag = text.lastIndexOf(">");
51
+
52
+ // If last > is after last <, we're in text content
53
+ if (lastCloseTag > lastOpenTag) {
54
+ return true;
55
+ }
56
+
57
+ return false;
58
+ }
59
+
60
+ /**
61
+ * Calculate indentation for XML, properly handling template expressions.
62
+ * @internal Exported for testing
63
+ */
64
+ export function calculateXmlIndentation(
65
+ textBefore: string,
66
+ indentUnit: number,
67
+ ): number {
68
+ let depth = 0;
69
+
70
+ // Match all opening and closing tags
71
+ const tagRegex = /<\/?[\w:-]+[^>]*\/?>/g;
72
+ let match;
73
+
74
+ while ((match = tagRegex.exec(textBefore)) !== null) {
75
+ const tag = match[0];
76
+
77
+ // Self-closing tag: <tag />
78
+ if (tag.endsWith("/>")) {
79
+ // No depth change
80
+ continue;
81
+ }
82
+
83
+ // Closing tag: </tag>
84
+ if (tag.startsWith("</")) {
85
+ depth = Math.max(0, depth - 1);
86
+ continue;
87
+ }
88
+
89
+ // Opening tag: <tag> or <tag attr="value">
90
+ depth++;
91
+ }
92
+
93
+ return depth * indentUnit;
94
+ }