@checkstack/ui 0.2.3 → 0.3.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 (30) hide show
  1. package/CHANGELOG.md +48 -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.test.ts +271 -0
  9. package/src/components/CodeEditor/languageSupport/json.ts +240 -0
  10. package/src/components/CodeEditor/languageSupport/markdown.test.ts +245 -0
  11. package/src/components/CodeEditor/languageSupport/markdown.ts +183 -0
  12. package/src/components/CodeEditor/languageSupport/types.ts +48 -0
  13. package/src/components/CodeEditor/languageSupport/xml.test.ts +236 -0
  14. package/src/components/CodeEditor/languageSupport/xml.ts +194 -0
  15. package/src/components/CodeEditor/languageSupport/yaml.test.ts +200 -0
  16. package/src/components/CodeEditor/languageSupport/yaml.ts +205 -0
  17. package/src/components/DynamicForm/DynamicForm.tsx +2 -24
  18. package/src/components/DynamicForm/DynamicOptionsField.tsx +48 -21
  19. package/src/components/DynamicForm/FormField.tsx +38 -70
  20. package/src/components/DynamicForm/JsonField.tsx +19 -25
  21. package/src/components/DynamicForm/KeyValueEditor.tsx +314 -0
  22. package/src/components/DynamicForm/MultiTypeEditorField.tsx +465 -0
  23. package/src/components/DynamicForm/index.ts +13 -0
  24. package/src/components/DynamicForm/types.ts +14 -8
  25. package/src/components/DynamicForm/utils.test.ts +390 -0
  26. package/src/components/DynamicForm/utils.ts +142 -3
  27. package/src/components/StrategyConfigCard.tsx +8 -4
  28. package/src/index.ts +1 -1
  29. package/src/components/TemplateEditor.test.ts +0 -156
  30. 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";
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,183 @@
1
+ import { markdown } from "@codemirror/lang-markdown";
2
+ import { Decoration } from "@codemirror/view";
3
+ import type { LanguageSupport, DecorationRange } from "./types";
4
+
5
+ // Decoration marks for Markdown syntax highlighting using inline styles
6
+ const mdHeadingMark = Decoration.mark({
7
+ attributes: { style: "color: hsl(280, 65%, 60%); font-weight: bold" },
8
+ });
9
+ const mdBoldMark = Decoration.mark({
10
+ attributes: { style: "font-weight: bold" },
11
+ });
12
+ const mdItalicMark = Decoration.mark({
13
+ attributes: { style: "font-style: italic" },
14
+ });
15
+ const mdCodeMark = Decoration.mark({
16
+ attributes: {
17
+ style:
18
+ "color: hsl(142.1, 76.2%, 36.3%); background: hsla(0, 0%, 50%, 0.1); border-radius: 3px; padding: 0 2px",
19
+ },
20
+ });
21
+ const mdLinkMark = Decoration.mark({
22
+ attributes: { style: "color: hsl(217.2, 91.2%, 59.8%)" },
23
+ });
24
+ const templateMark = Decoration.mark({
25
+ attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
26
+ });
27
+
28
+ /**
29
+ * Check if cursor is in a valid Markdown template position.
30
+ * Templates can appear almost anywhere in Markdown (text content).
31
+ * @internal Exported for testing
32
+ */
33
+ export function isValidMarkdownTemplatePosition(text: string): boolean {
34
+ // In Markdown, templates are valid almost everywhere except:
35
+ // - Inside code blocks (``` ... ```)
36
+ // - Inside inline code (` ... `)
37
+
38
+ // Check for unclosed code blocks
39
+ const codeBlockMatches = text.match(/```/g) || [];
40
+ if (codeBlockMatches.length % 2 === 1) {
41
+ return false; // Inside a code block
42
+ }
43
+
44
+ // Check if we're inside inline code on the current line
45
+ const lastNewline = text.lastIndexOf("\n");
46
+ const currentLine = text.slice(lastNewline + 1);
47
+ const backticks = (currentLine.match(/`/g) || []).length;
48
+
49
+ // If odd number of backticks, we're inside inline code
50
+ if (backticks % 2 === 1) {
51
+ return false;
52
+ }
53
+
54
+ // Valid in all other positions
55
+ return true;
56
+ }
57
+
58
+ /**
59
+ * Build Markdown + template decorations.
60
+ */
61
+ function buildMarkdownDecorations(doc: string): DecorationRange[] {
62
+ const ranges: DecorationRange[] = [];
63
+
64
+ // Match templates first (highest priority)
65
+ const templateRegex = /\{\{[\w.[\]]*\}\}/g;
66
+ let match;
67
+ while ((match = templateRegex.exec(doc)) !== null) {
68
+ ranges.push({
69
+ from: match.index,
70
+ to: match.index + match[0].length,
71
+ decoration: templateMark,
72
+ });
73
+ }
74
+
75
+ // Match headings (# ... or ## ... etc.)
76
+ const headingRegex = /^(#{1,6})\s+.+$/gm;
77
+ while ((match = headingRegex.exec(doc)) !== null) {
78
+ ranges.push({
79
+ from: match.index,
80
+ to: match.index + match[0].length,
81
+ decoration: mdHeadingMark,
82
+ });
83
+ }
84
+
85
+ // Match bold (**text** or __text__)
86
+ const boldRegex = /(\*\*|__)(?!\s)(.+?)(?<!\s)\1/g;
87
+ while ((match = boldRegex.exec(doc)) !== null) {
88
+ ranges.push({
89
+ from: match.index,
90
+ to: match.index + match[0].length,
91
+ decoration: mdBoldMark,
92
+ });
93
+ }
94
+
95
+ // Match italic (*text* or _text_) - but not **
96
+ const italicRegex = /(?<!\*|\w)(\*|_)(?!\*|\s)(.+?)(?<!\s|\*)\1(?!\*|\w)/g;
97
+ while ((match = italicRegex.exec(doc)) !== null) {
98
+ ranges.push({
99
+ from: match.index,
100
+ to: match.index + match[0].length,
101
+ decoration: mdItalicMark,
102
+ });
103
+ }
104
+
105
+ // Match inline code (`code`)
106
+ const codeRegex = /`[^`\n]+`/g;
107
+ while ((match = codeRegex.exec(doc)) !== null) {
108
+ ranges.push({
109
+ from: match.index,
110
+ to: match.index + match[0].length,
111
+ decoration: mdCodeMark,
112
+ });
113
+ }
114
+
115
+ // Match links [text](url)
116
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
117
+ while ((match = linkRegex.exec(doc)) !== null) {
118
+ ranges.push({
119
+ from: match.index,
120
+ to: match.index + match[0].length,
121
+ decoration: mdLinkMark,
122
+ });
123
+ }
124
+
125
+ // Sort by position
126
+ ranges.sort((a, b) => a.from - b.from || a.to - b.to);
127
+
128
+ // Remove overlaps (templates take priority)
129
+ const filtered: DecorationRange[] = [];
130
+ for (const range of ranges) {
131
+ const overlaps = filtered.some(
132
+ (existing) =>
133
+ (range.from >= existing.from && range.from < existing.to) ||
134
+ (range.to > existing.from && range.to <= existing.to),
135
+ );
136
+ if (!overlaps) {
137
+ filtered.push(range);
138
+ }
139
+ }
140
+
141
+ return filtered;
142
+ }
143
+
144
+ /**
145
+ * Calculate indentation for Markdown.
146
+ * Markdown uses list indentation.
147
+ * @internal Exported for testing
148
+ */
149
+ export function calculateMarkdownIndentation(
150
+ textBefore: string,
151
+ _indentUnit: number,
152
+ ): number {
153
+ const lines = textBefore.split("\n");
154
+ if (lines.length === 0) return 0;
155
+
156
+ // Find the last non-empty line
157
+ for (let i = lines.length - 1; i >= 0; i--) {
158
+ const line = lines[i];
159
+ if (line.trim().length === 0) continue;
160
+
161
+ // Count leading spaces
162
+ const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
163
+
164
+ // If line is a list item, next line should be indented if continuing
165
+ if (/^\s*[-*+]\s/.test(line) || /^\s*\d+\.\s/.test(line)) {
166
+ return leadingSpaces;
167
+ }
168
+
169
+ return leadingSpaces;
170
+ }
171
+
172
+ return 0;
173
+ }
174
+
175
+ /**
176
+ * Markdown language support for CodeEditor with template expression handling.
177
+ */
178
+ export const markdownLanguageSupport: LanguageSupport = {
179
+ extension: markdown(),
180
+ buildDecorations: buildMarkdownDecorations,
181
+ isValidTemplatePosition: isValidMarkdownTemplatePosition,
182
+ calculateIndentation: calculateMarkdownIndentation,
183
+ };
@@ -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
+ }