@checkstack/ui 1.10.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 (41) hide show
  1. package/CHANGELOG.md +384 -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/TemplateInput.tsx +104 -0
  27. package/src/components/TemplateInputToggle.tsx +111 -0
  28. package/src/components/TemplateValueInput.test.ts +98 -0
  29. package/src/components/TemplateValueInput.tsx +470 -0
  30. package/src/components/VariablePicker.tsx +271 -0
  31. package/src/hooks/useInitOnceForKey.test.ts +27 -0
  32. package/src/hooks/useInitOnceForKey.ts +21 -18
  33. package/src/index.ts +5 -0
  34. package/stories/ActionCard.stories.tsx +62 -0
  35. package/stories/Alert.stories.tsx +5 -5
  36. package/stories/TemplateInputToggle.stories.tsx +77 -0
  37. package/stories/TemplateValueInput.stories.tsx +65 -0
  38. package/stories/VariablePicker.stories.tsx +109 -0
  39. package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
  40. package/src/components/CodeEditor/monacoStdlib.ts +0 -62
  41. package/src/components/CodeEditor/monacoWorkers.ts +0 -118
@@ -0,0 +1,271 @@
1
+ import React from "react";
2
+ import { ChevronRight, Code2, Search } from "lucide-react";
3
+ import { Input } from "./Input";
4
+ import { Popover, PopoverContent, PopoverTrigger } from "./Popover";
5
+
6
+ /**
7
+ * One node in the variable scope tree.
8
+ *
9
+ * Mirrors `VariableEntry` from `@checkstack/automation-common` — but this
10
+ * package can't depend on automation-common (UI primitives live below the
11
+ * automation layer), so the shape is duplicated here. The automation
12
+ * editor adapts `VariableScope.entries` → `VariableNode[]` at the call
13
+ * site.
14
+ */
15
+ export interface VariableNode {
16
+ /** Canonical dot-separated path, e.g. `artifact.integration-jira.issue.key`. */
17
+ path: string;
18
+ /**
19
+ * Runtime-parseable `{{ }}` insertion form, e.g.
20
+ * `artifacts["integration-jira.issue"].key`. Inserted in preference to
21
+ * `path`; callers that don't supply it fall back to `path`.
22
+ */
23
+ templateRef?: string;
24
+ /** Human-readable type label rendered next to the name. */
25
+ type: string;
26
+ description?: string;
27
+ /** Nested entries for object types. */
28
+ children?: VariableNode[];
29
+ /**
30
+ * When set, the entry only exists when one of these triggers fired —
31
+ * picker renders an "Only when …" hint so the operator knows it might
32
+ * be undefined at runtime.
33
+ */
34
+ conditionalOnTriggers?: string[];
35
+ }
36
+
37
+ export interface VariablePickerProps {
38
+ /** Tree of in-scope variables. */
39
+ scope: VariableNode[];
40
+ /**
41
+ * Called with the runtime-parseable text to insert (the node's
42
+ * `templateRef`, falling back to `path`), e.g.
43
+ * `trigger.payload.systemId` or `artifacts["integration-jira.issue"].key`.
44
+ */
45
+ onSelect: (templateRef: string) => void;
46
+ /** Optional element rendered as the popover trigger. Defaults to a small "Insert variable" button. */
47
+ trigger?: React.ReactNode;
48
+ /** Controls open state; defaults to uncontrolled. */
49
+ open?: boolean;
50
+ onOpenChange?: (open: boolean) => void;
51
+ /** Optional class on the popover content. */
52
+ contentClassName?: string;
53
+ }
54
+
55
+ /**
56
+ * Hierarchical variable picker. Click the trigger button → a popover
57
+ * opens with the tree of in-scope paths grouped under their parents
58
+ * (`trigger`, `var`, `artifact`, `repeat`). Each row inserts
59
+ * `{{ path }}` when clicked. A search input at the top filters across
60
+ * the whole tree (matches against the dot-path), and matched ancestors
61
+ * stay expanded so context is preserved.
62
+ *
63
+ * Designed for the explicit "fx" / "Insert variable" workflow inside the
64
+ * automation editor. For inline `{{` autocomplete inside a text field,
65
+ * use `TemplateValueInput`; for code editors, the `CodeEditor` already
66
+ * pops the same dropdown through Monaco's completion provider.
67
+ */
68
+ export const VariablePicker: React.FC<VariablePickerProps> = ({
69
+ scope,
70
+ onSelect,
71
+ trigger,
72
+ open,
73
+ onOpenChange,
74
+ contentClassName,
75
+ }) => {
76
+ const [query, setQuery] = React.useState("");
77
+ const [internalOpen, setInternalOpen] = React.useState(false);
78
+ const isOpen = open ?? internalOpen;
79
+ const setOpen = (next: boolean) => {
80
+ if (open === undefined) setInternalOpen(next);
81
+ onOpenChange?.(next);
82
+ };
83
+
84
+ const filtered = React.useMemo(
85
+ () => (query.trim() ? filterTree(scope, query.trim().toLowerCase()) : scope),
86
+ [scope, query],
87
+ );
88
+
89
+ const handleSelect = (path: string) => {
90
+ onSelect(path);
91
+ setOpen(false);
92
+ setQuery("");
93
+ };
94
+
95
+ const handleKeyDownOnInput = (e: React.KeyboardEvent<HTMLInputElement>) => {
96
+ if (e.key === "Escape") {
97
+ e.preventDefault();
98
+ setOpen(false);
99
+ }
100
+ };
101
+
102
+ return (
103
+ <Popover open={isOpen} onOpenChange={setOpen}>
104
+ <PopoverTrigger asChild>
105
+ {trigger ?? (
106
+ <button
107
+ type="button"
108
+ className="inline-flex items-center gap-1 rounded border border-border bg-card px-2 py-1 text-xs font-mono text-muted-foreground hover:bg-accent hover:text-accent-foreground"
109
+ aria-label="Insert variable"
110
+ >
111
+ <Code2 className="h-3 w-3" />
112
+ <span>fx</span>
113
+ </button>
114
+ )}
115
+ </PopoverTrigger>
116
+ <PopoverContent
117
+ className={`w-80 p-0 ${contentClassName ?? ""}`.trim()}
118
+ align="start"
119
+ >
120
+ <div className="border-b border-border px-2 py-2">
121
+ <div className="relative">
122
+ <Search className="absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
123
+ <Input
124
+ autoFocus
125
+ value={query}
126
+ onChange={(e) => setQuery(e.target.value)}
127
+ onKeyDown={handleKeyDownOnInput}
128
+ placeholder="Filter variables…"
129
+ className="h-7 pl-7 font-mono text-xs"
130
+ />
131
+ </div>
132
+ </div>
133
+ <div className="max-h-80 overflow-y-auto py-1">
134
+ {filtered.length === 0 ? (
135
+ <p className="px-3 py-4 text-center text-xs text-muted-foreground italic">
136
+ No matching variables.
137
+ </p>
138
+ ) : (
139
+ filtered.map((node) => (
140
+ <VariableTreeNode
141
+ key={node.path}
142
+ node={node}
143
+ depth={0}
144
+ onSelect={handleSelect}
145
+ forceExpand={query.trim().length > 0}
146
+ />
147
+ ))
148
+ )}
149
+ </div>
150
+ </PopoverContent>
151
+ </Popover>
152
+ );
153
+ };
154
+
155
+ interface VariableTreeNodeProps {
156
+ node: VariableNode;
157
+ depth: number;
158
+ onSelect: (path: string) => void;
159
+ forceExpand: boolean;
160
+ }
161
+
162
+ const VariableTreeNode: React.FC<VariableTreeNodeProps> = ({
163
+ node,
164
+ depth,
165
+ onSelect,
166
+ forceExpand,
167
+ }) => {
168
+ const [expanded, setExpanded] = React.useState(depth === 0);
169
+ const isExpanded = forceExpand || expanded;
170
+ const hasChildren = node.children && node.children.length > 0;
171
+
172
+ return (
173
+ <div>
174
+ <div
175
+ className="group flex w-full items-center gap-1 text-xs hover:bg-accent/50"
176
+ style={{ paddingLeft: `${depth * 12 + 8}px` }}
177
+ >
178
+ {hasChildren ? (
179
+ <button
180
+ type="button"
181
+ onClick={(event) => {
182
+ event.stopPropagation();
183
+ setExpanded((v) => !v);
184
+ }}
185
+ className="flex h-6 w-4 shrink-0 items-center justify-center rounded hover:bg-accent hover:text-accent-foreground"
186
+ aria-label={isExpanded ? "Collapse" : "Expand"}
187
+ aria-expanded={isExpanded}
188
+ >
189
+ <ChevronRight
190
+ className={`h-3 w-3 transition-transform ${
191
+ isExpanded ? "rotate-90" : ""
192
+ }`}
193
+ />
194
+ </button>
195
+ ) : (
196
+ <span className="h-6 w-4 shrink-0" />
197
+ )}
198
+ <button
199
+ type="button"
200
+ onClick={() => onSelect(node.templateRef ?? node.path)}
201
+ className="flex flex-1 items-center gap-2 py-1 pr-2 text-left hover:bg-accent hover:text-accent-foreground"
202
+ title={`Insert {{${node.templateRef ?? node.path}}}`}
203
+ >
204
+ <code className="flex-1 truncate font-mono">{leafName(node.path)}</code>
205
+ <span className="shrink-0 font-mono text-[10px] text-muted-foreground">
206
+ {node.type}
207
+ </span>
208
+ </button>
209
+ </div>
210
+ {node.conditionalOnTriggers && node.conditionalOnTriggers.length > 0 && (
211
+ <p
212
+ className="px-2 text-[10px] italic text-muted-foreground"
213
+ style={{ paddingLeft: `${depth * 12 + 24}px` }}
214
+ >
215
+ Only when {node.conditionalOnTriggers.join(", ")}
216
+ </p>
217
+ )}
218
+ {node.description && (
219
+ <p
220
+ className="px-2 pb-1 text-[10px] text-muted-foreground"
221
+ style={{ paddingLeft: `${depth * 12 + 24}px` }}
222
+ >
223
+ {node.description}
224
+ </p>
225
+ )}
226
+ {hasChildren && isExpanded && (
227
+ <div>
228
+ {node.children!.map((child) => (
229
+ <VariableTreeNode
230
+ key={child.path}
231
+ node={child}
232
+ depth={depth + 1}
233
+ onSelect={onSelect}
234
+ forceExpand={forceExpand}
235
+ />
236
+ ))}
237
+ </div>
238
+ )}
239
+ </div>
240
+ );
241
+ };
242
+
243
+ function leafName(path: string): string {
244
+ const idx = path.lastIndexOf(".");
245
+ return idx === -1 ? path : path.slice(idx + 1);
246
+ }
247
+
248
+ /**
249
+ * Walk the tree and return a copy that only includes nodes whose path
250
+ * (or any descendant's path) contains the query. Matched ancestors are
251
+ * kept so the tree structure is preserved.
252
+ */
253
+ function filterTree(
254
+ nodes: VariableNode[],
255
+ query: string,
256
+ ): VariableNode[] {
257
+ const out: VariableNode[] = [];
258
+ for (const node of nodes) {
259
+ const selfMatch = node.path.toLowerCase().includes(query);
260
+ const childMatches = node.children
261
+ ? filterTree(node.children, query)
262
+ : undefined;
263
+ if (selfMatch || (childMatches && childMatches.length > 0)) {
264
+ out.push({
265
+ ...node,
266
+ children: childMatches,
267
+ });
268
+ }
269
+ }
270
+ return out;
271
+ }
@@ -124,4 +124,31 @@ describe("shouldInitForKey", () => {
124
124
  shouldInitForKey({ value: "any", key: "1", initialisedKey: 1 }),
125
125
  ).toBe(true);
126
126
  });
127
+
128
+ it("does not seed from a stale cache entry served on mount, only from a post-mount fetch", () => {
129
+ // Regression for the automation editor: another page observes the same
130
+ // `getAutomation` cache key without `gcTime: 0`, so a stale (pre-edit)
131
+ // entry can be served instantly on reopen. The call site gates `value` on
132
+ // `isFetchedAfterMount`, so until a fresh fetch lands it passes `undefined`
133
+ // and we must NOT seed the stale value (which would revert edits such as a
134
+ // renamed trigger id back to its auto-generated default).
135
+ const staleCacheBeforeFreshFetch = undefined; // isFetchedAfterMount === false
136
+ expect(
137
+ shouldInitForKey({
138
+ value: staleCacheBeforeFreshFetch,
139
+ key: "auto-1",
140
+ initialisedKey: undefined,
141
+ }),
142
+ ).toBe(false);
143
+
144
+ // Once the post-mount fetch resolves, the caller passes the fresh value and
145
+ // the one-shot init fires with server-truth data.
146
+ expect(
147
+ shouldInitForKey({
148
+ value: { name: "A", definition: { triggers: [{ id: "majors" }] } },
149
+ key: "auto-1",
150
+ initialisedKey: undefined,
151
+ }),
152
+ ).toBe(true);
153
+ });
127
154
  });
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef } from "react";
1
+ import { useState } from "react";
2
2
 
3
3
  /**
4
4
  * Pure decision function powering {@link useInitOnceForKey}. Extracted so
@@ -52,8 +52,15 @@ export function shouldInitForKey({
52
52
  * - Skips initialisation entirely while either `value` or `key` is
53
53
  * `undefined`/`null`.
54
54
  *
55
- * `onInit` is read from a ref so callers can safely pass a fresh closure
56
- * each render without re-firing the effect.
55
+ * Seeding runs **during render** (not in a `useEffect`). This is deliberate:
56
+ * the app is wrapped in `<StrictMode>`, which double-mounts components and
57
+ * **discards a `setState` scheduled from an effect** when the source query
58
+ * resolves synchronously — e.g. a warm react-query cache on reopen. That made
59
+ * the one-shot init silently no-op, reverting seeded form state (a renamed id
60
+ * snapping back to its default, etc.) while a cold-cache first open worked.
61
+ * Seeding during render with a state guard is React's recommended
62
+ * "adjust state when data changes" pattern and is immune to that race. `onInit`
63
+ * must therefore be pure aside from calling this component's state setters.
57
64
  *
58
65
  * @example
59
66
  * useInitOnceForKey(existingConfig, existingConfig?.id, (config) => {
@@ -68,20 +75,16 @@ export function useInitOnceForKey<T>(
68
75
  key: string | number | null | undefined,
69
76
  onInit: (value: T) => void,
70
77
  ): void {
71
- const initialisedKeyRef = useRef<string | number | null | undefined>(undefined);
72
- const onInitRef = useRef(onInit);
78
+ const [initialisedKey, setInitialisedKey] =
79
+ useState<string | number | null | undefined>();
73
80
 
74
- // Keep the latest callback in a ref so a fresh closure each render doesn't
75
- // re-trigger the effect; only `value` and `key` should drive it.
76
- useEffect(() => {
77
- onInitRef.current = onInit;
78
- });
79
-
80
- useEffect(() => {
81
- if (!shouldInitForKey({ value, key, initialisedKey: initialisedKeyRef.current })) {
82
- return;
83
- }
84
- initialisedKeyRef.current = key;
85
- onInitRef.current(value as T);
86
- }, [value, key]);
81
+ if (shouldInitForKey({ value, key, initialisedKey })) {
82
+ // Setting state + invoking `onInit` (which sets this component's state)
83
+ // during render is the supported "store info from previous renders"
84
+ // pattern: React restarts this component's render with the new state
85
+ // before committing, and the `initialisedKey` guard makes it idempotent
86
+ // (no loop; background refetches of the same key are ignored).
87
+ setInitialisedKey(key);
88
+ onInit(value as T);
89
+ }
87
90
  }
package/src/index.ts CHANGED
@@ -56,6 +56,11 @@ export * from "./components/TerminalFeed";
56
56
  export * from "./components/PerformanceProvider";
57
57
  export * from "./components/AmbientBackground";
58
58
  export * from "./components/CodeEditor";
59
+ export * from "./components/TemplateValueInput";
60
+ export * from "./components/VariablePicker";
61
+ export * from "./components/TemplateInput";
62
+ export * from "./components/TemplateInputToggle";
63
+ export * from "./components/ActionCard";
59
64
  export * from "./components/AnimatedNumber";
60
65
  export * from "./hooks/useAnimatedNumber";
61
66
  export * from "./components/IDELayout";
@@ -0,0 +1,62 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { ActionCard } from "../src/components/ActionCard";
4
+
5
+ const meta: Meta<typeof ActionCard> = {
6
+ title: "Components/Automation/ActionCard",
7
+ component: ActionCard,
8
+ tags: ["autodocs"],
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ "Collapsible card that hosts a single action in the visual automation editor.",
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof ActionCard>;
21
+
22
+ const FullFeaturedDemo = () => {
23
+ const [enabled, setEnabled] = useState(true);
24
+ return (
25
+ <div className="w-[680px] p-4">
26
+ <ActionCard
27
+ id="notify-1"
28
+ title="Notify User"
29
+ description="Send a transactional notification to a specific operator."
30
+ category="Notification"
31
+ icon="Bell"
32
+ enabled={enabled}
33
+ onEnabledChange={setEnabled}
34
+ onDelete={() => alert("Delete clicked")}
35
+ badges={[{ label: "produces: notify_user_result", variant: "secondary" }]}
36
+ >
37
+ <div className="space-y-2 text-sm">
38
+ <p className="text-muted-foreground">
39
+ (DynamicForm or per-action config UI goes here.)
40
+ </p>
41
+ <code className="block font-mono text-xs">userId, title, body</code>
42
+ </div>
43
+ </ActionCard>
44
+ </div>
45
+ );
46
+ };
47
+
48
+ export const FullFeatured: Story = {
49
+ render: () => <FullFeaturedDemo />,
50
+ };
51
+
52
+ export const MinimalNoToggle: Story = {
53
+ render: () => (
54
+ <div className="w-[680px] p-4">
55
+ <ActionCard id="log-1" title="Log" icon="FileText">
56
+ <p className="text-sm text-muted-foreground">
57
+ Write a single line to the run logger.
58
+ </p>
59
+ </ActionCard>
60
+ </div>
61
+ ),
62
+ };
@@ -1,5 +1,5 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
- import { CircleAlert, CircleCheck, Info, TriangleAlert } from "lucide-react";
2
+ import { CircleAlert, CircleCheck, Info, AlertTriangle } from "lucide-react";
3
3
  import {
4
4
  Alert,
5
5
  AlertContent,
@@ -28,7 +28,7 @@ export const Default: Story = {
28
28
  render: (args) => (
29
29
  <Alert {...args}>
30
30
  <AlertIcon>
31
- <Info className="h-4 w-4" />
31
+ <Info className="w-4 h-4" />
32
32
  </AlertIcon>
33
33
  <AlertContent>
34
34
  <AlertTitle>Heads up</AlertTitle>
@@ -45,7 +45,7 @@ export const Success: Story = {
45
45
  render: (args) => (
46
46
  <Alert {...args}>
47
47
  <AlertIcon>
48
- <CircleCheck className="h-4 w-4" />
48
+ <CircleCheck className="w-4 h-4" />
49
49
  </AlertIcon>
50
50
  <AlertContent>
51
51
  <AlertTitle>Saved</AlertTitle>
@@ -60,7 +60,7 @@ export const Warning: Story = {
60
60
  render: (args) => (
61
61
  <Alert {...args}>
62
62
  <AlertIcon>
63
- <TriangleAlert className="h-4 w-4" />
63
+ <AlertTriangle className="w-4 h-4" />
64
64
  </AlertIcon>
65
65
  <AlertContent>
66
66
  <AlertTitle>Heads up</AlertTitle>
@@ -77,7 +77,7 @@ export const Error: Story = {
77
77
  render: (args) => (
78
78
  <Alert {...args}>
79
79
  <AlertIcon>
80
- <CircleAlert className="h-4 w-4" />
80
+ <CircleAlert className="w-4 h-4" />
81
81
  </AlertIcon>
82
82
  <AlertContent>
83
83
  <AlertTitle>Something went wrong</AlertTitle>
@@ -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
+ };