@checkstack/ui 1.9.0 → 1.11.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 (52) hide show
  1. package/CHANGELOG.md +417 -0
  2. package/package.json +15 -7
  3. package/scripts/generate-stdlib-types.ts +2 -2
  4. package/src/components/ActionCard.tsx +221 -0
  5. package/src/components/CodeEditor/CodeEditor.tsx +51 -9
  6. package/src/components/CodeEditor/TypefoxEditor.tsx +868 -0
  7. package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
  8. package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
  9. package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
  10. package/src/components/CodeEditor/index.ts +2 -0
  11. package/src/components/CodeEditor/scriptContext.test.ts +41 -0
  12. package/src/components/CodeEditor/scriptContext.ts +76 -1
  13. package/src/components/CodeEditor/templateValidation.ts +51 -0
  14. package/src/components/CodeEditor/types.ts +109 -0
  15. package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
  16. package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
  17. package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
  18. package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
  19. package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
  20. package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
  21. package/src/components/DynamicForm/DynamicForm.tsx +2 -0
  22. package/src/components/DynamicForm/FormField.tsx +29 -9
  23. package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
  24. package/src/components/DynamicForm/MultiTypeEditorField.tsx +16 -7
  25. package/src/components/DynamicForm/types.ts +11 -0
  26. package/src/components/ListEmptyState.tsx +51 -0
  27. package/src/components/QueryErrorState.tsx +64 -0
  28. package/src/components/ResponsiveTable.tsx +92 -0
  29. package/src/components/Skeleton.tsx +39 -0
  30. package/src/components/TemplateInput.tsx +104 -0
  31. package/src/components/TemplateInputToggle.tsx +111 -0
  32. package/src/components/TemplateValueInput.test.ts +98 -0
  33. package/src/components/TemplateValueInput.tsx +470 -0
  34. package/src/components/VariablePicker.tsx +271 -0
  35. package/src/hooks/useInitOnceForKey.test.ts +27 -0
  36. package/src/hooks/useInitOnceForKey.ts +21 -18
  37. package/src/index.ts +10 -0
  38. package/src/utils/toastTemplates.test.ts +82 -0
  39. package/src/utils/toastTemplates.ts +47 -0
  40. package/stories/ActionCard.stories.tsx +62 -0
  41. package/stories/Alert.stories.tsx +5 -5
  42. package/stories/ListEmptyState.stories.tsx +48 -0
  43. package/stories/QueryErrorState.stories.tsx +40 -0
  44. package/stories/ResponsiveTable.stories.tsx +93 -0
  45. package/stories/Skeleton.stories.tsx +53 -0
  46. package/stories/TemplateInputToggle.stories.tsx +77 -0
  47. package/stories/TemplateValueInput.stories.tsx +65 -0
  48. package/stories/VariablePicker.stories.tsx +109 -0
  49. package/stories/toastTemplates.stories.tsx +60 -0
  50. package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
  51. package/src/components/CodeEditor/monacoStdlib.ts +0 -62
  52. package/src/components/CodeEditor/monacoWorkers.ts +0 -118
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { QueryErrorState } from "../src/components/QueryErrorState";
3
+
4
+ const meta: Meta<typeof QueryErrorState> = {
5
+ title: "Components/Feedback/QueryErrorState",
6
+ component: QueryErrorState,
7
+ tags: ["autodocs"],
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof QueryErrorState>;
12
+
13
+ export const Default: Story = {
14
+ args: {
15
+ error: new Error("Failed to reach the backend at /api/health-checks"),
16
+ onRetry: () => {
17
+ // Storybook noop - wires up to a real refetch() in the app
18
+ console.log("retry clicked");
19
+ },
20
+ },
21
+ };
22
+
23
+ export const WithResource: Story = {
24
+ args: {
25
+ error: new Error("Network request failed (504)"),
26
+ resource: "checks",
27
+ onRetry: () => {
28
+ console.log("retry clicked");
29
+ },
30
+ },
31
+ };
32
+
33
+ export const NonErrorPayload: Story = {
34
+ args: {
35
+ error: null,
36
+ onRetry: () => {
37
+ console.log("retry clicked");
38
+ },
39
+ },
40
+ };
@@ -0,0 +1,93 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Badge } from "../src/components/Badge";
3
+ import {
4
+ MobileCardList,
5
+ ResponsiveTable,
6
+ } from "../src/components/ResponsiveTable";
7
+ import {
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableHead,
12
+ TableHeader,
13
+ TableRow,
14
+ } from "../src/components/Table";
15
+
16
+ const meta: Meta = {
17
+ title: "Components/Data/ResponsiveTable",
18
+ };
19
+
20
+ export default meta;
21
+ type Story = StoryObj;
22
+
23
+ interface Row {
24
+ name: string;
25
+ strategy: string;
26
+ status: "healthy" | "degraded" | "down";
27
+ latency: string;
28
+ }
29
+
30
+ const rows: Row[] = [
31
+ { name: "api.checkstack.io", strategy: "HTTP probe", status: "healthy", latency: "42 ms" },
32
+ { name: "db-primary", strategy: "TCP probe", status: "degraded", latency: "340 ms" },
33
+ { name: "cache", strategy: "HTTP probe", status: "down", latency: "—" },
34
+ ];
35
+
36
+ const variantFor = (status: Row["status"]) => {
37
+ if (status === "healthy") return "success" as const;
38
+ if (status === "degraded") return "warning" as const;
39
+ return "destructive" as const;
40
+ };
41
+
42
+ export const DualLayout: Story = {
43
+ render: () => (
44
+ <div className="w-full">
45
+ <ResponsiveTable>
46
+ <Table>
47
+ <TableHeader>
48
+ <TableRow>
49
+ <TableHead>Name</TableHead>
50
+ <TableHead>Strategy</TableHead>
51
+ <TableHead>Status</TableHead>
52
+ <TableHead className="text-right">Latency</TableHead>
53
+ </TableRow>
54
+ </TableHeader>
55
+ <TableBody>
56
+ {rows.map((row) => (
57
+ <TableRow key={row.name}>
58
+ <TableCell>{row.name}</TableCell>
59
+ <TableCell>{row.strategy}</TableCell>
60
+ <TableCell>
61
+ <Badge variant={variantFor(row.status)}>{row.status}</Badge>
62
+ </TableCell>
63
+ <TableCell className="text-right">{row.latency}</TableCell>
64
+ </TableRow>
65
+ ))}
66
+ </TableBody>
67
+ </Table>
68
+ </ResponsiveTable>
69
+
70
+ <MobileCardList>
71
+ {rows.map((row) => (
72
+ <div
73
+ key={row.name}
74
+ className="rounded-md border border-border bg-card p-3 flex flex-col gap-1"
75
+ >
76
+ <div className="flex items-center justify-between">
77
+ <span className="font-medium">{row.name}</span>
78
+ <Badge variant={variantFor(row.status)}>{row.status}</Badge>
79
+ </div>
80
+ <div className="text-xs text-muted-foreground">
81
+ {row.strategy} · {row.latency}
82
+ </div>
83
+ </div>
84
+ ))}
85
+ </MobileCardList>
86
+
87
+ <p className="text-xs text-muted-foreground mt-4">
88
+ Resize the viewport: the table appears on <code>sm</code> and up, the
89
+ stacked card list shows on smaller screens.
90
+ </p>
91
+ </div>
92
+ ),
93
+ };
@@ -0,0 +1,53 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Skeleton } from "../src/components/Skeleton";
3
+
4
+ const meta: Meta<typeof Skeleton> = {
5
+ title: "Components/Feedback/Skeleton",
6
+ component: Skeleton,
7
+ tags: ["autodocs"],
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof Skeleton>;
12
+
13
+ export const Default: Story = {
14
+ args: {
15
+ className: "h-4 w-48",
16
+ },
17
+ };
18
+
19
+ export const TextBlock: Story = {
20
+ render: () => (
21
+ <div className="space-y-2 max-w-md">
22
+ <Skeleton className="h-4 w-3/4" />
23
+ <Skeleton className="h-4 w-full" />
24
+ <Skeleton className="h-4 w-5/6" />
25
+ </div>
26
+ ),
27
+ };
28
+
29
+ export const ListRow: Story = {
30
+ render: () => (
31
+ <div className="flex items-center gap-3 max-w-md">
32
+ <Skeleton className="h-10 w-10 rounded-full" />
33
+ <div className="flex-1 space-y-2">
34
+ <Skeleton className="h-4 w-1/2" />
35
+ <Skeleton className="h-3 w-1/3" />
36
+ </div>
37
+ </div>
38
+ ),
39
+ };
40
+
41
+ export const CardGrid: Story = {
42
+ render: () => (
43
+ <div className="grid grid-cols-2 gap-3 max-w-xl">
44
+ {Array.from({ length: 4 }, (_, index) => (
45
+ <div key={index} className="rounded-md border border-border p-4 space-y-3">
46
+ <Skeleton className="h-5 w-2/3" />
47
+ <Skeleton className="h-4 w-full" />
48
+ <Skeleton className="h-4 w-5/6" />
49
+ </div>
50
+ ))}
51
+ </div>
52
+ ),
53
+ };
@@ -0,0 +1,77 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { TemplateInputToggle } from "../src/components/TemplateInputToggle";
4
+ import { Input } from "../src/components/Input";
5
+
6
+ const meta: Meta<typeof TemplateInputToggle> = {
7
+ title: "Components/Automation/TemplateInputToggle",
8
+ component: TemplateInputToggle,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ 'Wraps a typed input with an "fx" button. Click it to switch into template mode and back.',
15
+ },
16
+ },
17
+ },
18
+ };
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof TemplateInputToggle>;
22
+
23
+ const NumberFieldDemo = () => {
24
+ const [value, setValue] = useState("30");
25
+ return (
26
+ <div className="w-96 p-4">
27
+ <TemplateInputToggle
28
+ value={value}
29
+ onChange={setValue}
30
+ renderTyped={({ disabled }) => (
31
+ <Input
32
+ type="number"
33
+ value={value}
34
+ onChange={(e) => setValue(e.target.value)}
35
+ disabled={disabled}
36
+ />
37
+ )}
38
+ templateProperties={[
39
+ { path: "trigger.payload.timeoutSeconds", type: "number" },
40
+ { path: "var.delay", type: "number" },
41
+ ]}
42
+ templatePlaceholder="{{ trigger.payload.timeoutSeconds }}"
43
+ />
44
+ </div>
45
+ );
46
+ };
47
+
48
+ const StartsInTemplateModeDemo = () => {
49
+ const [value, setValue] = useState("{{ trigger.payload.timeoutSeconds }}");
50
+ return (
51
+ <div className="w-96 p-4">
52
+ <TemplateInputToggle
53
+ value={value}
54
+ onChange={setValue}
55
+ renderTyped={({ disabled }) => (
56
+ <Input
57
+ type="number"
58
+ value={value}
59
+ onChange={(e) => setValue(e.target.value)}
60
+ disabled={disabled}
61
+ />
62
+ )}
63
+ templateProperties={[
64
+ { path: "trigger.payload.timeoutSeconds", type: "number" },
65
+ ]}
66
+ />
67
+ </div>
68
+ );
69
+ };
70
+
71
+ export const NumberField: Story = {
72
+ render: () => <NumberFieldDemo />,
73
+ };
74
+
75
+ export const StartsInTemplateMode: Story = {
76
+ render: () => <StartsInTemplateModeDemo />,
77
+ };
@@ -0,0 +1,65 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { TemplateValueInput } from "../src/components/TemplateValueInput";
4
+ import type { TemplateProperty } from "../src/components/CodeEditor";
5
+
6
+ const meta: Meta<typeof TemplateValueInput> = {
7
+ title: "Components/Automation/TemplateValueInput",
8
+ component: TemplateValueInput,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ "Single-line input with `{{` template autocomplete. Type `{{` to open the picker.",
15
+ },
16
+ },
17
+ },
18
+ };
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof TemplateValueInput>;
22
+
23
+ const sampleProperties: TemplateProperty[] = [
24
+ { path: "trigger.event", type: "string", description: "Event id." },
25
+ { path: "trigger.payload.incidentId", type: "string" },
26
+ { path: "trigger.payload.title", type: "string" },
27
+ { path: "trigger.payload.severity", type: '"low" | "high"' },
28
+ { path: "var.outer", type: "string" },
29
+ { path: "artifact.jira.issue.key", type: "string" },
30
+ ];
31
+
32
+ const WithPropertiesDemo = () => {
33
+ const [value, setValue] = useState("Title: ");
34
+ return (
35
+ <div className="w-96 p-4">
36
+ <TemplateValueInput
37
+ value={value}
38
+ onChange={setValue}
39
+ placeholder="Type {{ to insert a variable…"
40
+ templateProperties={sampleProperties}
41
+ />
42
+ </div>
43
+ );
44
+ };
45
+
46
+ const NoPropertiesDemo = () => {
47
+ const [value, setValue] = useState("");
48
+ return (
49
+ <div className="w-96 p-4">
50
+ <TemplateValueInput
51
+ value={value}
52
+ onChange={setValue}
53
+ placeholder="No template autocomplete here"
54
+ />
55
+ </div>
56
+ );
57
+ };
58
+
59
+ export const WithProperties: Story = {
60
+ render: () => <WithPropertiesDemo />,
61
+ };
62
+
63
+ export const NoProperties: Story = {
64
+ render: () => <NoPropertiesDemo />,
65
+ };
@@ -0,0 +1,109 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import {
4
+ VariablePicker,
5
+ type VariableNode,
6
+ } from "../src/components/VariablePicker";
7
+
8
+ const meta: Meta<typeof VariablePicker> = {
9
+ title: "Components/Automation/VariablePicker",
10
+ component: VariablePicker,
11
+ tags: ["autodocs"],
12
+ parameters: {
13
+ docs: {
14
+ description: {
15
+ component:
16
+ "Hierarchical picker — click the trigger button to open a tree of in-scope automation variables.",
17
+ },
18
+ },
19
+ },
20
+ };
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof VariablePicker>;
24
+
25
+ const sampleScope: VariableNode[] = [
26
+ {
27
+ path: "trigger",
28
+ type: "object",
29
+ description: "Trigger that fired this run.",
30
+ children: [
31
+ {
32
+ path: "trigger.event",
33
+ type: '"incident.created" | "incident.resolved"',
34
+ description: "Discriminator.",
35
+ },
36
+ {
37
+ path: "trigger.payload",
38
+ type: "object",
39
+ children: [
40
+ { path: "trigger.payload.incidentId", type: "string" },
41
+ {
42
+ path: "trigger.payload.title",
43
+ type: "string",
44
+ conditionalOnTriggers: ["incident.created"],
45
+ },
46
+ {
47
+ path: "trigger.payload.resolvedAt",
48
+ type: "string",
49
+ conditionalOnTriggers: ["incident.resolved"],
50
+ },
51
+ ],
52
+ },
53
+ ],
54
+ },
55
+ {
56
+ path: "var",
57
+ templateRef: "variables",
58
+ type: "object",
59
+ description: "Operator-defined variables.",
60
+ children: [
61
+ { path: "var.outer", templateRef: "variables.outer", type: "string" },
62
+ ],
63
+ },
64
+ {
65
+ path: "artifact",
66
+ templateRef: "artifacts",
67
+ type: "object",
68
+ description: "Artifacts produced upstream.",
69
+ children: [
70
+ {
71
+ // Hyphenated/dotted artifact id → templateRef uses bracket notation
72
+ // so the inserted `{{ }}` text is runtime-parseable.
73
+ path: "artifact.integration-jira.issue",
74
+ templateRef: 'artifacts["integration-jira.issue"]',
75
+ type: "object",
76
+ children: [
77
+ {
78
+ path: "artifact.integration-jira.issue.key",
79
+ templateRef: 'artifacts["integration-jira.issue"].key',
80
+ type: "string",
81
+ },
82
+ {
83
+ path: "artifact.integration-jira.issue.url",
84
+ templateRef: 'artifacts["integration-jira.issue"].url',
85
+ type: "string",
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ },
91
+ ];
92
+
93
+ const DefaultDemo = () => {
94
+ const [inserted, setInserted] = useState<string | null>(null);
95
+ return (
96
+ <div className="p-12 flex flex-col items-start gap-4">
97
+ <VariablePicker scope={sampleScope} onSelect={setInserted} />
98
+ {inserted && (
99
+ <p className="text-xs text-muted-foreground">
100
+ Inserted: <code className="font-mono">{`{{${inserted}}}`}</code>
101
+ </p>
102
+ )}
103
+ </div>
104
+ );
105
+ };
106
+
107
+ export const Default: Story = {
108
+ render: () => <DefaultDemo />,
109
+ };
@@ -0,0 +1,60 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Button } from "../src/components/Button";
3
+ import { useToast } from "../src/components/ToastProvider";
4
+ import { toastError, toastSuccess } from "../src/utils/toastTemplates";
5
+
6
+ const meta: Meta = {
7
+ title: "Foundations/toastTemplates",
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj;
12
+
13
+ const Demo = () => {
14
+ const toast = useToast();
15
+
16
+ return (
17
+ <div className="space-y-3 max-w-md">
18
+ <p className="text-sm text-muted-foreground">
19
+ Fire the canonical success / error toasts. The error helper
20
+ prefixes the action and truncates long messages to 100 characters.
21
+ </p>
22
+
23
+ <div className="flex flex-wrap gap-2">
24
+ <Button onClick={() => toastSuccess(toast, "Check created")}>
25
+ Fire success
26
+ </Button>
27
+
28
+ <Button
29
+ variant="destructive"
30
+ onClick={() =>
31
+ toastError(
32
+ toast,
33
+ "Failed to create check",
34
+ new Error("Backend unreachable"),
35
+ )
36
+ }
37
+ >
38
+ Fire error
39
+ </Button>
40
+
41
+ <Button
42
+ variant="outline"
43
+ onClick={() =>
44
+ toastError(
45
+ toast,
46
+ "Failed to import config",
47
+ new Error(
48
+ "Validation failed: " + "x".repeat(300),
49
+ ),
50
+ )
51
+ }
52
+ >
53
+ Fire truncated error
54
+ </Button>
55
+ </div>
56
+ </div>
57
+ );
58
+ };
59
+
60
+ export const Helpers: Story = { render: () => <Demo /> };