@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 CHANGED
@@ -1,5 +1,43 @@
1
1
  # @checkstack/ui
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d1324e6: Enhanced DateTimePicker with calendar popup and independent field editing
8
+
9
+ - Added calendar popup using `react-day-picker` and Radix Popover for date selection
10
+ - Implemented independent input fields for day, month, year, hour, and minute
11
+ - Added input validation with proper clamping on blur (respects leap years)
12
+ - Updated `onChange` signature to `Date | undefined` to handle invalid states
13
+ - Fixed Dialog focus ring clipping by adding wrapper with negative margin/padding
14
+
15
+ ### Patch Changes
16
+
17
+ - 2c0822d: ### Queue System
18
+
19
+ - Added cron pattern support to `scheduleRecurring()` - accepts either `intervalSeconds` or `cronPattern`
20
+ - BullMQ backend uses native cron scheduling via `pattern` option
21
+ - InMemoryQueue implements wall-clock cron scheduling with `cron-parser`
22
+
23
+ ### Maintenance Backend
24
+
25
+ - Auto status transitions now use cron pattern `* * * * *` for precise second-0 scheduling
26
+ - User notifications are now sent for auto-started and auto-completed maintenances
27
+ - Refactored to call `addUpdate` RPC for status changes, centralizing hook/signal/notification logic
28
+
29
+ ### UI
30
+
31
+ - DateTimePicker now resets seconds and milliseconds to 0 when time is changed
32
+
33
+ ## 0.3.1
34
+
35
+ ### Patch Changes
36
+
37
+ - Updated dependencies [8a87cd4]
38
+ - @checkstack/common@0.5.0
39
+ - @checkstack/frontend-api@0.3.2
40
+
3
41
  ## 0.3.0
4
42
 
5
43
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/ui",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "dependencies": {
@@ -16,6 +16,7 @@
16
16
  "@codemirror/view": "^6.39.11",
17
17
  "@radix-ui/react-accordion": "^1.2.12",
18
18
  "@radix-ui/react-dialog": "^1.1.15",
19
+ "@radix-ui/react-popover": "^1.1.15",
19
20
  "@radix-ui/react-select": "^2.2.6",
20
21
  "@radix-ui/react-slot": "^1.2.4",
21
22
  "@uiw/react-codemirror": "^4.25.4",
@@ -26,6 +27,7 @@
26
27
  "date-fns": "^4.1.0",
27
28
  "lucide-react": "0.562.0",
28
29
  "react": "^18.2.0",
30
+ "react-day-picker": "^9.13.0",
29
31
  "react-markdown": "^10.1.0",
30
32
  "react-router-dom": "^6.20.0",
31
33
  "recharts": "^3.6.0",
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Pure utility functions for JSON 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 JSON template position.
8
+ * Returns true if:
9
+ * 1. Inside a string value (for string templates like "{{payload.name}}")
10
+ * 2. In a value position after a colon but outside quotes (for number templates like {{payload.count}})
11
+ * 3. Inside an array element position
12
+ *
13
+ * @internal Exported for testing
14
+ */
15
+ export function isValidJsonTemplatePosition(text: string): boolean {
16
+ let insideString = false;
17
+ let inValuePosition = false;
18
+ // Stack to track object/array nesting: 'o' for object, 'a' for array
19
+ const nestingStack: ("o" | "a")[] = [];
20
+
21
+ for (let i = 0; i < text.length; i++) {
22
+ const char = text[i];
23
+ const prevChar = i > 0 ? text[i - 1] : "";
24
+
25
+ if (char === '"' && prevChar !== "\\") {
26
+ insideString = !insideString;
27
+ } else if (!insideString) {
28
+ switch (char) {
29
+ case ":": {
30
+ inValuePosition = true;
31
+ break;
32
+ }
33
+ case ",": {
34
+ // After comma: in arrays we stay in value position, in objects we go to key position
35
+ const currentContext = nestingStack.at(-1);
36
+ inValuePosition = currentContext === "a";
37
+ break;
38
+ }
39
+ case "{": {
40
+ // Start of object - next is a key position, not a value
41
+ nestingStack.push("o");
42
+ inValuePosition = false;
43
+ break;
44
+ }
45
+ case "[": {
46
+ // Start of array - next is a value position
47
+ nestingStack.push("a");
48
+ inValuePosition = true;
49
+ break;
50
+ }
51
+ case "}": {
52
+ nestingStack.pop();
53
+ inValuePosition = false;
54
+ break;
55
+ }
56
+ case "]": {
57
+ nestingStack.pop();
58
+ inValuePosition = false;
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ // Valid if inside a string OR in a value position (for bare number templates)
66
+ return insideString || inValuePosition;
67
+ }
68
+
69
+ /**
70
+ * Calculate indentation for JSON, properly handling template expressions.
71
+ * Counts structural { and [ while ignoring {{ template braces.
72
+ * @internal Exported for testing
73
+ */
74
+ export function calculateJsonIndentation(
75
+ textBefore: string,
76
+ indentUnit: number,
77
+ ): number {
78
+ let depth = 0;
79
+ let insideString = false;
80
+ let i = 0;
81
+
82
+ while (i < textBefore.length) {
83
+ const char = textBefore[i];
84
+ const nextChar = i < textBefore.length - 1 ? textBefore[i + 1] : "";
85
+ const prevChar = i > 0 ? textBefore[i - 1] : "";
86
+
87
+ // Handle string boundaries
88
+ if (char === '"' && prevChar !== "\\") {
89
+ insideString = !insideString;
90
+ i++;
91
+ continue;
92
+ }
93
+
94
+ if (!insideString) {
95
+ // Skip template braces {{ and }}
96
+ if (char === "{" && nextChar === "{") {
97
+ i += 2;
98
+ continue;
99
+ }
100
+ if (char === "}" && nextChar === "}") {
101
+ i += 2;
102
+ continue;
103
+ }
104
+
105
+ // Count structural braces
106
+ if (char === "{" || char === "[") {
107
+ depth++;
108
+ } else if (char === "}" || char === "]") {
109
+ depth = Math.max(0, depth - 1);
110
+ }
111
+ }
112
+
113
+ i++;
114
+ }
115
+
116
+ return depth * indentUnit;
117
+ }
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
 
3
- import { isValidJsonTemplatePosition, calculateJsonIndentation } from "./json";
3
+ import {
4
+ isValidJsonTemplatePosition,
5
+ calculateJsonIndentation,
6
+ } from "./json-utils";
4
7
 
5
8
  describe("isValidJsonTemplatePosition", () => {
6
9
  describe("string values (inside quotes)", () => {
@@ -2,6 +2,12 @@ import { json } from "@codemirror/lang-json";
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
+ isValidJsonTemplatePosition,
8
+ calculateJsonIndentation,
9
+ } from "./json-utils";
10
+
5
11
  // Decoration marks for JSON syntax highlighting using inline styles
6
12
  // (higher specificity than Lezer parser's CSS classes)
7
13
  const jsonStringMark = Decoration.mark({
@@ -20,69 +26,6 @@ const templateMark = Decoration.mark({
20
26
  attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
21
27
  });
22
28
 
23
- /**
24
- * Check if cursor is in a valid JSON template position.
25
- * Returns true if:
26
- * 1. Inside a string value (for string templates like "{{payload.name}}")
27
- * 2. In a value position after a colon but outside quotes (for number templates like {{payload.count}})
28
- * 3. Inside an array element position
29
- *
30
- * @internal Exported for testing
31
- */
32
- export function isValidJsonTemplatePosition(text: string): boolean {
33
- let insideString = false;
34
- let inValuePosition = false;
35
- // Stack to track object/array nesting: 'o' for object, 'a' for array
36
- const nestingStack: ("o" | "a")[] = [];
37
-
38
- for (let i = 0; i < text.length; i++) {
39
- const char = text[i];
40
- const prevChar = i > 0 ? text[i - 1] : "";
41
-
42
- if (char === '"' && prevChar !== "\\") {
43
- insideString = !insideString;
44
- } else if (!insideString) {
45
- switch (char) {
46
- case ":": {
47
- inValuePosition = true;
48
- break;
49
- }
50
- case ",": {
51
- // After comma: in arrays we stay in value position, in objects we go to key position
52
- const currentContext = nestingStack.at(-1);
53
- inValuePosition = currentContext === "a";
54
- break;
55
- }
56
- case "{": {
57
- // Start of object - next is a key position, not a value
58
- nestingStack.push("o");
59
- inValuePosition = false;
60
- break;
61
- }
62
- case "[": {
63
- // Start of array - next is a value position
64
- nestingStack.push("a");
65
- inValuePosition = true;
66
- break;
67
- }
68
- case "}": {
69
- nestingStack.pop();
70
- inValuePosition = false;
71
- break;
72
- }
73
- case "]": {
74
- nestingStack.pop();
75
- inValuePosition = false;
76
- break;
77
- }
78
- }
79
- }
80
- }
81
-
82
- // Valid if inside a string OR in a value position (for bare number templates)
83
- return insideString || inValuePosition;
84
- }
85
-
86
29
  /**
87
30
  * Build comprehensive JSON + template decorations.
88
31
  * This uses regex-based matching to apply consistent syntax highlighting
@@ -179,55 +122,11 @@ function buildJsonDecorations(doc: string): DecorationRange[] {
179
122
  return filtered;
180
123
  }
181
124
 
182
- /**
183
- * Calculate indentation for JSON, properly handling template expressions.
184
- * Counts structural { and [ while ignoring {{ template braces.
185
- * @internal Exported for testing
186
- */
187
- export function calculateJsonIndentation(
188
- textBefore: string,
189
- indentUnit: number,
190
- ): number {
191
- let depth = 0;
192
- let insideString = false;
193
- let i = 0;
194
-
195
- while (i < textBefore.length) {
196
- const char = textBefore[i];
197
- const nextChar = i < textBefore.length - 1 ? textBefore[i + 1] : "";
198
- const prevChar = i > 0 ? textBefore[i - 1] : "";
199
-
200
- // Handle string boundaries
201
- if (char === '"' && prevChar !== "\\") {
202
- insideString = !insideString;
203
- i++;
204
- continue;
205
- }
206
-
207
- if (!insideString) {
208
- // Skip template braces {{ and }}
209
- if (char === "{" && nextChar === "{") {
210
- i += 2;
211
- continue;
212
- }
213
- if (char === "}" && nextChar === "}") {
214
- i += 2;
215
- continue;
216
- }
217
-
218
- // Count structural braces
219
- if (char === "{" || char === "[") {
220
- depth++;
221
- } else if (char === "}" || char === "]") {
222
- depth = Math.max(0, depth - 1);
223
- }
224
- }
225
-
226
- i++;
227
- }
228
-
229
- return depth * indentUnit;
230
- }
125
+ // Import the pure utils for use in the language support object
126
+ import {
127
+ isValidJsonTemplatePosition,
128
+ calculateJsonIndentation,
129
+ } from "./json-utils";
231
130
 
232
131
  /**
233
132
  * JSON language support for CodeEditor with template expression handling.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Pure utility functions for Markdown 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 Markdown template position.
8
+ * Templates can appear almost anywhere in Markdown (text content).
9
+ * @internal Exported for testing
10
+ */
11
+ export function isValidMarkdownTemplatePosition(text: string): boolean {
12
+ // In Markdown, templates are valid almost everywhere except:
13
+ // - Inside code blocks (``` ... ```)
14
+ // - Inside inline code (` ... `)
15
+
16
+ // Check for unclosed code blocks
17
+ const codeBlockMatches = text.match(/```/g) || [];
18
+ if (codeBlockMatches.length % 2 === 1) {
19
+ return false; // Inside a code block
20
+ }
21
+
22
+ // Check if we're inside inline code on the current line
23
+ const lastNewline = text.lastIndexOf("\n");
24
+ const currentLine = text.slice(lastNewline + 1);
25
+ const backticks = (currentLine.match(/`/g) || []).length;
26
+
27
+ // If odd number of backticks, we're inside inline code
28
+ if (backticks % 2 === 1) {
29
+ return false;
30
+ }
31
+
32
+ // Valid in all other positions
33
+ return true;
34
+ }
35
+
36
+ /**
37
+ * Calculate indentation for Markdown.
38
+ * Markdown uses list indentation.
39
+ * @internal Exported for testing
40
+ */
41
+ export function calculateMarkdownIndentation(
42
+ textBefore: string,
43
+ _indentUnit: number,
44
+ ): number {
45
+ const lines = textBefore.split("\n");
46
+ if (lines.length === 0) return 0;
47
+
48
+ // Find the last non-empty line
49
+ for (let i = lines.length - 1; i >= 0; i--) {
50
+ const line = lines[i];
51
+ if (line.trim().length === 0) continue;
52
+
53
+ // Count leading spaces
54
+ const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
55
+
56
+ // If line is a list item, next line should be indented if continuing
57
+ if (/^\s*[-*+]\s/.test(line) || /^\s*\d+\.\s/.test(line)) {
58
+ return leadingSpaces;
59
+ }
60
+
61
+ return leadingSpaces;
62
+ }
63
+
64
+ return 0;
65
+ }
@@ -3,7 +3,7 @@ import { describe, expect, it } from "bun:test";
3
3
  import {
4
4
  isValidMarkdownTemplatePosition,
5
5
  calculateMarkdownIndentation,
6
- } from "./markdown";
6
+ } from "./markdown-utils";
7
7
 
8
8
  describe("isValidMarkdownTemplatePosition", () => {
9
9
  describe("valid text positions", () => {
@@ -2,6 +2,12 @@ import { markdown } from "@codemirror/lang-markdown";
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
+ isValidMarkdownTemplatePosition,
8
+ calculateMarkdownIndentation,
9
+ } from "./markdown-utils";
10
+
5
11
  // Decoration marks for Markdown syntax highlighting using inline styles
6
12
  const mdHeadingMark = Decoration.mark({
7
13
  attributes: { style: "color: hsl(280, 65%, 60%); font-weight: bold" },
@@ -25,36 +31,6 @@ const templateMark = Decoration.mark({
25
31
  attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
26
32
  });
27
33
 
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
34
  /**
59
35
  * Build Markdown + template decorations.
60
36
  */
@@ -141,36 +117,11 @@ function buildMarkdownDecorations(doc: string): DecorationRange[] {
141
117
  return filtered;
142
118
  }
143
119
 
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
- }
120
+ // Import the pure utils for use in the language support object
121
+ import {
122
+ isValidMarkdownTemplatePosition,
123
+ calculateMarkdownIndentation,
124
+ } from "./markdown-utils";
174
125
 
175
126
  /**
176
127
  * Markdown language support for CodeEditor with template expression handling.
@@ -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
+ }
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
 
3
- import { isValidXmlTemplatePosition, calculateXmlIndentation } from "./xml";
3
+ import {
4
+ isValidXmlTemplatePosition,
5
+ calculateXmlIndentation,
6
+ } from "./xml-utils";
4
7
 
5
8
  describe("isValidXmlTemplatePosition", () => {
6
9
  describe("text content positions", () => {