@checkstack/ui 0.3.0 → 0.4.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/CHANGELOG.md +38 -0
- package/package.json +3 -1
- package/src/components/CodeEditor/languageSupport/json-utils.ts +117 -0
- package/src/components/CodeEditor/languageSupport/json.test.ts +4 -1
- package/src/components/CodeEditor/languageSupport/json.ts +11 -112
- package/src/components/CodeEditor/languageSupport/markdown-utils.ts +65 -0
- package/src/components/CodeEditor/languageSupport/markdown.test.ts +1 -1
- package/src/components/CodeEditor/languageSupport/markdown.ts +11 -60
- package/src/components/CodeEditor/languageSupport/xml-utils.ts +94 -0
- package/src/components/CodeEditor/languageSupport/xml.test.ts +4 -1
- package/src/components/CodeEditor/languageSupport/xml.ts +11 -89
- package/src/components/CodeEditor/languageSupport/yaml-utils.ts +101 -0
- package/src/components/CodeEditor/languageSupport/yaml.test.ts +4 -1
- package/src/components/CodeEditor/languageSupport/yaml.ts +11 -96
- package/src/components/DateRangeFilter.tsx +12 -4
- package/src/components/DateTimePicker.tsx +332 -39
- package/src/components/Dialog.tsx +10 -8
|
@@ -2,6 +2,12 @@ import { xml } from "@codemirror/lang-xml";
|
|
|
2
2
|
import { Decoration } from "@codemirror/view";
|
|
3
3
|
import type { LanguageSupport, DecorationRange } from "./types";
|
|
4
4
|
|
|
5
|
+
// Re-export pure utils for backwards compatibility
|
|
6
|
+
export {
|
|
7
|
+
isValidXmlTemplatePosition,
|
|
8
|
+
calculateXmlIndentation,
|
|
9
|
+
} from "./xml-utils";
|
|
10
|
+
|
|
5
11
|
// Decoration marks for XML syntax highlighting using inline styles
|
|
6
12
|
const xmlTagMark = Decoration.mark({
|
|
7
13
|
attributes: { style: "color: hsl(280, 65%, 60%)" },
|
|
@@ -16,60 +22,6 @@ const templateMark = Decoration.mark({
|
|
|
16
22
|
attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
|
|
17
23
|
});
|
|
18
24
|
|
|
19
|
-
/**
|
|
20
|
-
* Check if cursor is in a valid XML template position.
|
|
21
|
-
* Templates can appear in:
|
|
22
|
-
* 1. Text content between tags: <tag>|</tag>
|
|
23
|
-
* 2. Attribute values: <tag attr="|">
|
|
24
|
-
* @internal Exported for testing
|
|
25
|
-
*/
|
|
26
|
-
export function isValidXmlTemplatePosition(text: string): boolean {
|
|
27
|
-
// Check if we're inside an attribute value (inside quotes after =)
|
|
28
|
-
let inTag = false;
|
|
29
|
-
let inAttrValue = false;
|
|
30
|
-
let quoteChar = "";
|
|
31
|
-
|
|
32
|
-
for (let i = 0; i < text.length; i++) {
|
|
33
|
-
const char = text[i];
|
|
34
|
-
|
|
35
|
-
if (!inTag && char === "<" && text[i + 1] !== "/") {
|
|
36
|
-
inTag = true;
|
|
37
|
-
inAttrValue = false;
|
|
38
|
-
} else if (inTag && char === ">") {
|
|
39
|
-
inTag = false;
|
|
40
|
-
inAttrValue = false;
|
|
41
|
-
} else if (inTag && !inAttrValue && char === "=") {
|
|
42
|
-
// Next should be a quote
|
|
43
|
-
const nextChar = text[i + 1];
|
|
44
|
-
if (nextChar === '"' || nextChar === "'") {
|
|
45
|
-
quoteChar = nextChar;
|
|
46
|
-
inAttrValue = true;
|
|
47
|
-
i++; // Skip the quote
|
|
48
|
-
}
|
|
49
|
-
} else if (inAttrValue && char === quoteChar) {
|
|
50
|
-
inAttrValue = false;
|
|
51
|
-
quoteChar = "";
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Valid if inside attribute value
|
|
56
|
-
if (inAttrValue) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Valid if between tags (text content position)
|
|
61
|
-
// Check if we're after a closing > and not inside an opening <
|
|
62
|
-
const lastOpenTag = text.lastIndexOf("<");
|
|
63
|
-
const lastCloseTag = text.lastIndexOf(">");
|
|
64
|
-
|
|
65
|
-
// If last > is after last <, we're in text content
|
|
66
|
-
if (lastCloseTag > lastOpenTag) {
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
25
|
/**
|
|
74
26
|
* Build XML + template decorations.
|
|
75
27
|
*/
|
|
@@ -147,41 +99,11 @@ function buildXmlDecorations(doc: string): DecorationRange[] {
|
|
|
147
99
|
return filtered;
|
|
148
100
|
}
|
|
149
101
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
textBefore: string,
|
|
156
|
-
indentUnit: number,
|
|
157
|
-
): number {
|
|
158
|
-
let depth = 0;
|
|
159
|
-
|
|
160
|
-
// Match all opening and closing tags
|
|
161
|
-
const tagRegex = /<\/?[\w:-]+[^>]*\/?>/g;
|
|
162
|
-
let match;
|
|
163
|
-
|
|
164
|
-
while ((match = tagRegex.exec(textBefore)) !== null) {
|
|
165
|
-
const tag = match[0];
|
|
166
|
-
|
|
167
|
-
// Self-closing tag: <tag />
|
|
168
|
-
if (tag.endsWith("/>")) {
|
|
169
|
-
// No depth change
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Closing tag: </tag>
|
|
174
|
-
if (tag.startsWith("</")) {
|
|
175
|
-
depth = Math.max(0, depth - 1);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Opening tag: <tag> or <tag attr="value">
|
|
180
|
-
depth++;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return depth * indentUnit;
|
|
184
|
-
}
|
|
102
|
+
// Import the pure utils for use in the language support object
|
|
103
|
+
import {
|
|
104
|
+
isValidXmlTemplatePosition,
|
|
105
|
+
calculateXmlIndentation,
|
|
106
|
+
} from "./xml-utils";
|
|
185
107
|
|
|
186
108
|
/**
|
|
187
109
|
* XML language support for CodeEditor with template expression handling.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utility functions for YAML 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 YAML template position.
|
|
8
|
+
* In YAML, templates can appear as values after "key: ".
|
|
9
|
+
* @internal Exported for testing
|
|
10
|
+
*/
|
|
11
|
+
export function isValidYamlTemplatePosition(text: string): boolean {
|
|
12
|
+
// Find the current line
|
|
13
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
14
|
+
const currentLine = text.slice(lastNewline + 1);
|
|
15
|
+
|
|
16
|
+
// Valid positions in YAML:
|
|
17
|
+
// 1. After a colon and space (value position): "key: |"
|
|
18
|
+
// 2. Inside a quoted string: 'key: "hello|'
|
|
19
|
+
// 3. In a list item position: "- |"
|
|
20
|
+
|
|
21
|
+
// Check if we're inside a quoted string
|
|
22
|
+
const singleQuotes = (currentLine.match(/'/g) || []).length;
|
|
23
|
+
const doubleQuotes = (currentLine.match(/"/g) || []).length;
|
|
24
|
+
if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if we're after "key: " (value position)
|
|
29
|
+
const colonMatch = currentLine.match(/^\s*[\w-]+:\s+/);
|
|
30
|
+
if (colonMatch) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if we're in a list item position "- "
|
|
35
|
+
const listMatch = currentLine.match(/^\s*-\s+/);
|
|
36
|
+
if (listMatch) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if we're after ": " anywhere in the line
|
|
41
|
+
if (currentLine.includes(": ")) {
|
|
42
|
+
const afterColon = currentLine.split(": ").slice(1).join(": ");
|
|
43
|
+
// If we're in the value part (after colon)
|
|
44
|
+
if (afterColon.length > 0 || currentLine.endsWith(": ")) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Calculate indentation for YAML, properly handling template expressions.
|
|
54
|
+
* YAML uses indentation-based structure.
|
|
55
|
+
* @internal Exported for testing
|
|
56
|
+
*/
|
|
57
|
+
export function calculateYamlIndentation(
|
|
58
|
+
textBefore: string,
|
|
59
|
+
indentUnit: number,
|
|
60
|
+
): number {
|
|
61
|
+
const lines = textBefore.split("\n");
|
|
62
|
+
if (lines.length === 0) return 0;
|
|
63
|
+
|
|
64
|
+
// Get the last line (the one we're currently on after Enter)
|
|
65
|
+
const currentLine = lines.at(-1);
|
|
66
|
+
|
|
67
|
+
// If current line is empty or whitespace, look at previous line
|
|
68
|
+
if (currentLine?.trim().length === 0 && lines.length > 1) {
|
|
69
|
+
// Find the last non-empty line
|
|
70
|
+
for (let i = lines.length - 2; i >= 0; i--) {
|
|
71
|
+
const line = lines[i];
|
|
72
|
+
if (line.trim().length === 0) continue;
|
|
73
|
+
|
|
74
|
+
// Count leading spaces
|
|
75
|
+
const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
76
|
+
|
|
77
|
+
// If line ends with ":" it's a key that starts a new block
|
|
78
|
+
if (line.trimEnd().endsWith(":")) {
|
|
79
|
+
return leadingSpaces + indentUnit;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If line starts with "- " it's a list item
|
|
83
|
+
if (line.trim().startsWith("-")) {
|
|
84
|
+
return leadingSpaces + indentUnit;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Otherwise, maintain current indentation
|
|
88
|
+
return leadingSpaces;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// For non-empty current line, find previous non-empty for context
|
|
93
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
94
|
+
const line = lines[i];
|
|
95
|
+
if (line.trim().length === 0) continue;
|
|
96
|
+
const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
97
|
+
return leadingSpaces;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
isValidYamlTemplatePosition,
|
|
5
|
+
calculateYamlIndentation,
|
|
6
|
+
} from "./yaml-utils";
|
|
4
7
|
|
|
5
8
|
describe("isValidYamlTemplatePosition", () => {
|
|
6
9
|
describe("value positions", () => {
|
|
@@ -2,6 +2,12 @@ import { yaml } from "@codemirror/lang-yaml";
|
|
|
2
2
|
import { Decoration } from "@codemirror/view";
|
|
3
3
|
import type { LanguageSupport, DecorationRange } from "./types";
|
|
4
4
|
|
|
5
|
+
// Re-export pure utils for backwards compatibility
|
|
6
|
+
export {
|
|
7
|
+
isValidYamlTemplatePosition,
|
|
8
|
+
calculateYamlIndentation,
|
|
9
|
+
} from "./yaml-utils";
|
|
10
|
+
|
|
5
11
|
// Decoration marks for YAML syntax highlighting using inline styles
|
|
6
12
|
const yamlKeyMark = Decoration.mark({
|
|
7
13
|
attributes: { style: "color: hsl(210, 100%, 75%)" }, // Bright blue for better visibility
|
|
@@ -19,52 +25,6 @@ const templateMark = Decoration.mark({
|
|
|
19
25
|
attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
|
|
20
26
|
});
|
|
21
27
|
|
|
22
|
-
/**
|
|
23
|
-
* Check if cursor is in a valid YAML template position.
|
|
24
|
-
* In YAML, templates can appear as values after "key: ".
|
|
25
|
-
* @internal Exported for testing
|
|
26
|
-
*/
|
|
27
|
-
export function isValidYamlTemplatePosition(text: string): boolean {
|
|
28
|
-
// Find the current line
|
|
29
|
-
const lastNewline = text.lastIndexOf("\n");
|
|
30
|
-
const currentLine = text.slice(lastNewline + 1);
|
|
31
|
-
|
|
32
|
-
// Valid positions in YAML:
|
|
33
|
-
// 1. After a colon and space (value position): "key: |"
|
|
34
|
-
// 2. Inside a quoted string: 'key: "hello|'
|
|
35
|
-
// 3. In a list item position: "- |"
|
|
36
|
-
|
|
37
|
-
// Check if we're inside a quoted string
|
|
38
|
-
const singleQuotes = (currentLine.match(/'/g) || []).length;
|
|
39
|
-
const doubleQuotes = (currentLine.match(/"/g) || []).length;
|
|
40
|
-
if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1) {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Check if we're after "key: " (value position)
|
|
45
|
-
const colonMatch = currentLine.match(/^\s*[\w-]+:\s+/);
|
|
46
|
-
if (colonMatch) {
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Check if we're in a list item position "- "
|
|
51
|
-
const listMatch = currentLine.match(/^\s*-\s+/);
|
|
52
|
-
if (listMatch) {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Check if we're after ": " anywhere in the line
|
|
57
|
-
if (currentLine.includes(": ")) {
|
|
58
|
-
const afterColon = currentLine.split(": ").slice(1).join(": ");
|
|
59
|
-
// If we're in the value part (after colon)
|
|
60
|
-
if (afterColon.length > 0 || currentLine.endsWith(": ")) {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
28
|
/**
|
|
69
29
|
* Build YAML + template decorations.
|
|
70
30
|
*/
|
|
@@ -143,56 +103,11 @@ function buildYamlDecorations(doc: string): DecorationRange[] {
|
|
|
143
103
|
return filtered;
|
|
144
104
|
}
|
|
145
105
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
export function calculateYamlIndentation(
|
|
152
|
-
textBefore: string,
|
|
153
|
-
indentUnit: number,
|
|
154
|
-
): number {
|
|
155
|
-
const lines = textBefore.split("\n");
|
|
156
|
-
if (lines.length === 0) return 0;
|
|
157
|
-
|
|
158
|
-
// Get the last line (the one we're currently on after Enter)
|
|
159
|
-
const currentLine = lines.at(-1);
|
|
160
|
-
|
|
161
|
-
// If current line is empty or whitespace, look at previous line
|
|
162
|
-
if (currentLine?.trim().length === 0 && lines.length > 1) {
|
|
163
|
-
// Find the last non-empty line
|
|
164
|
-
for (let i = lines.length - 2; i >= 0; i--) {
|
|
165
|
-
const line = lines[i];
|
|
166
|
-
if (line.trim().length === 0) continue;
|
|
167
|
-
|
|
168
|
-
// Count leading spaces
|
|
169
|
-
const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
170
|
-
|
|
171
|
-
// If line ends with ":" it's a key that starts a new block
|
|
172
|
-
if (line.trimEnd().endsWith(":")) {
|
|
173
|
-
return leadingSpaces + indentUnit;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// If line starts with "- " it's a list item
|
|
177
|
-
if (line.trim().startsWith("-")) {
|
|
178
|
-
return leadingSpaces + indentUnit;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Otherwise, maintain current indentation
|
|
182
|
-
return leadingSpaces;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// For non-empty current line, find previous non-empty for context
|
|
187
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
188
|
-
const line = lines[i];
|
|
189
|
-
if (line.trim().length === 0) continue;
|
|
190
|
-
const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
191
|
-
return leadingSpaces;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return 0;
|
|
195
|
-
}
|
|
106
|
+
// Import the pure utils for use in the language support object
|
|
107
|
+
import {
|
|
108
|
+
isValidYamlTemplatePosition,
|
|
109
|
+
calculateYamlIndentation,
|
|
110
|
+
} from "./yaml-utils";
|
|
196
111
|
|
|
197
112
|
/**
|
|
198
113
|
* YAML language support for CodeEditor with template expression handling.
|
|
@@ -89,8 +89,8 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
|
89
89
|
activePreset === preset.id && !showCustom
|
|
90
90
|
? "primary"
|
|
91
91
|
: preset.id === "custom" && showCustom
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
? "primary"
|
|
93
|
+
: "outline"
|
|
94
94
|
}
|
|
95
95
|
size="sm"
|
|
96
96
|
onClick={() => handlePresetClick(preset.id)}
|
|
@@ -106,13 +106,21 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
|
106
106
|
<span className="text-sm text-muted-foreground">From:</span>
|
|
107
107
|
<DateTimePicker
|
|
108
108
|
value={value.startDate}
|
|
109
|
-
onChange={(startDate) =>
|
|
109
|
+
onChange={(startDate) => {
|
|
110
|
+
if (startDate) {
|
|
111
|
+
onChange({ ...value, startDate });
|
|
112
|
+
}
|
|
113
|
+
}}
|
|
110
114
|
maxDate={value.endDate}
|
|
111
115
|
/>
|
|
112
116
|
<span className="text-sm text-muted-foreground">To:</span>
|
|
113
117
|
<DateTimePicker
|
|
114
118
|
value={value.endDate}
|
|
115
|
-
onChange={(endDate) =>
|
|
119
|
+
onChange={(endDate) => {
|
|
120
|
+
if (endDate) {
|
|
121
|
+
onChange({ ...value, endDate });
|
|
122
|
+
}
|
|
123
|
+
}}
|
|
116
124
|
minDate={value.startDate}
|
|
117
125
|
maxDate={new Date()}
|
|
118
126
|
/>
|