@checkstack/ui 1.8.3 → 1.10.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 (36) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/package.json +6 -2
  3. package/scripts/generate-stdlib-types.ts +90 -0
  4. package/src/components/CodeEditor/CodeEditor.tsx +7 -0
  5. package/src/components/CodeEditor/MonacoEditor.tsx +203 -117
  6. package/src/components/CodeEditor/generateTypeDefinitions.ts +19 -26
  7. package/src/components/CodeEditor/generated/stdlib-types.json +1 -0
  8. package/src/components/CodeEditor/index.ts +7 -0
  9. package/src/components/CodeEditor/monacoStdlib.ts +62 -0
  10. package/src/components/CodeEditor/monacoWorkers.ts +118 -0
  11. package/src/components/CodeEditor/scriptContext.test.ts +280 -0
  12. package/src/components/CodeEditor/scriptContext.ts +467 -0
  13. package/src/components/CodeEditor/shellEnvVarMatcher.test.ts +95 -0
  14. package/src/components/CodeEditor/shellEnvVarMatcher.ts +70 -0
  15. package/src/components/DynamicForm/DynamicForm.tsx +6 -0
  16. package/src/components/DynamicForm/FormField.tsx +15 -0
  17. package/src/components/DynamicForm/MultiTypeEditorField.tsx +111 -6
  18. package/src/components/DynamicForm/index.ts +2 -0
  19. package/src/components/DynamicForm/starterTemplateSelector.test.ts +96 -0
  20. package/src/components/DynamicForm/starterTemplateSelector.ts +32 -0
  21. package/src/components/DynamicForm/types.ts +34 -1
  22. package/src/components/ListEmptyState.tsx +51 -0
  23. package/src/components/QueryErrorState.tsx +64 -0
  24. package/src/components/ResponsiveTable.tsx +92 -0
  25. package/src/components/Skeleton.tsx +39 -0
  26. package/src/hooks/useInitOnceForKey.test.ts +127 -0
  27. package/src/hooks/useInitOnceForKey.ts +87 -0
  28. package/src/index.ts +6 -0
  29. package/src/utils/toastTemplates.test.ts +82 -0
  30. package/src/utils/toastTemplates.ts +47 -0
  31. package/stories/ListEmptyState.stories.tsx +48 -0
  32. package/stories/QueryErrorState.stories.tsx +40 -0
  33. package/stories/ResponsiveTable.stories.tsx +93 -0
  34. package/stories/Skeleton.stories.tsx +53 -0
  35. package/stories/toastTemplates.stories.tsx +60 -0
  36. package/tsconfig.json +3 -1
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { shouldInitForKey } from "./useInitOnceForKey";
3
+
4
+ /**
5
+ * Regression suite for the form-preservation bug in the healthcheck editor.
6
+ *
7
+ * Before this hook existed, the page had:
8
+ *
9
+ * useEffect(() => {
10
+ * if (existingConfig) setFormState({ … });
11
+ * }, [existingConfig]);
12
+ *
13
+ * That fires on every refetch — including the refetches triggered by the
14
+ * realtime `HEALTH_CHECK_RUN_COMPLETED` signal — so the user's in-progress
15
+ * edits got wiped every time a healthcheck run completed anywhere on the
16
+ * platform.
17
+ *
18
+ * The hook delegates its decision to the pure `shouldInitForKey` function
19
+ * tested below. The hook is a thin React wrapper around two refs and a
20
+ * `useEffect` — testing the decision function gives full coverage of the
21
+ * logic without needing a DOM.
22
+ */
23
+
24
+ describe("shouldInitForKey", () => {
25
+ it("returns true the first time value+key are defined and no key has been initialised", () => {
26
+ expect(
27
+ shouldInitForKey({
28
+ value: { name: "A" },
29
+ key: "id-1",
30
+ initialisedKey: undefined,
31
+ }),
32
+ ).toBe(true);
33
+ });
34
+
35
+ it("returns false when value is undefined (query still loading)", () => {
36
+ expect(
37
+ shouldInitForKey({
38
+ value: undefined,
39
+ key: "id-1",
40
+ initialisedKey: undefined,
41
+ }),
42
+ ).toBe(false);
43
+ });
44
+
45
+ it("returns false when value is null", () => {
46
+ expect(
47
+ shouldInitForKey({
48
+ value: null,
49
+ key: "id-1",
50
+ initialisedKey: undefined,
51
+ }),
52
+ ).toBe(false);
53
+ });
54
+
55
+ it("returns false when key is undefined (no discriminator yet)", () => {
56
+ expect(
57
+ shouldInitForKey({
58
+ value: { name: "A" },
59
+ key: undefined,
60
+ initialisedKey: undefined,
61
+ }),
62
+ ).toBe(false);
63
+ });
64
+
65
+ it("returns false when key is null (record-not-found path)", () => {
66
+ expect(
67
+ shouldInitForKey({
68
+ value: { name: "A" },
69
+ key: null,
70
+ initialisedKey: undefined,
71
+ }),
72
+ ).toBe(false);
73
+ });
74
+
75
+ it("returns false on a background refetch (same key already initialised)", () => {
76
+ // THE regression case: react-query refetched the same record after an
77
+ // invalidation, handing us a new `value` object reference. The
78
+ // `initialisedKey` already matches the new key, so we must not re-fire.
79
+ expect(
80
+ shouldInitForKey({
81
+ value: { name: "A (refetched)" },
82
+ key: "id-1",
83
+ initialisedKey: "id-1",
84
+ }),
85
+ ).toBe(false);
86
+ });
87
+
88
+ it("returns true when the key changes (user navigated to a different record)", () => {
89
+ expect(
90
+ shouldInitForKey({
91
+ value: { name: "B" },
92
+ key: "id-2",
93
+ initialisedKey: "id-1",
94
+ }),
95
+ ).toBe(true);
96
+ });
97
+
98
+ it("returns true when we're returning to a previously-seen key from a different one", () => {
99
+ // The hook only stores the LAST initialised key, so flipping between
100
+ // two records re-initialises each time. That's the correct behaviour:
101
+ // each visit shows fresh server data.
102
+ expect(
103
+ shouldInitForKey({
104
+ value: { name: "A" },
105
+ key: "id-1",
106
+ initialisedKey: "id-2",
107
+ }),
108
+ ).toBe(true);
109
+ });
110
+
111
+ it("treats numeric keys correctly (different number → re-init)", () => {
112
+ expect(
113
+ shouldInitForKey({ value: "any", key: 1, initialisedKey: 1 }),
114
+ ).toBe(false);
115
+ expect(
116
+ shouldInitForKey({ value: "any", key: 2, initialisedKey: 1 }),
117
+ ).toBe(true);
118
+ });
119
+
120
+ it("treats string vs number keys as different (no implicit coercion)", () => {
121
+ // If a caller starts passing IDs as numbers after passing strings (or
122
+ // vice versa) we'd rather re-init than miss an update; strict !== handles it.
123
+ expect(
124
+ shouldInitForKey({ value: "any", key: "1", initialisedKey: 1 }),
125
+ ).toBe(true);
126
+ });
127
+ });
@@ -0,0 +1,87 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * Pure decision function powering {@link useInitOnceForKey}. Extracted so
5
+ * it can be unit-tested without a DOM (the hook itself wraps this with
6
+ * a `useRef` + `useEffect`).
7
+ *
8
+ * Returns `true` iff the caller should run their initialiser:
9
+ *
10
+ * - `value` is defined (the query has finished loading), AND
11
+ * - `key` is defined (we have a discriminator to track init-per-record),
12
+ * AND
13
+ * - we haven't yet initialised for this key (i.e. `initialisedKey !== key`).
14
+ *
15
+ * Background refetches of the same record produce a new `value` reference
16
+ * but the same `key`, so the function returns `false` for them — that's
17
+ * the whole point.
18
+ */
19
+ export function shouldInitForKey({
20
+ value,
21
+ key,
22
+ initialisedKey,
23
+ }: {
24
+ value: unknown;
25
+ key: string | number | null | undefined;
26
+ initialisedKey: string | number | null | undefined;
27
+ }): boolean {
28
+ if (value === undefined || value === null) return false;
29
+ if (key === undefined || key === null) return false;
30
+ return initialisedKey !== key;
31
+ }
32
+
33
+ /**
34
+ * Run a one-shot initialiser exactly once per `key`, ignoring subsequent
35
+ * `value` changes that keep the same key.
36
+ *
37
+ * Built for forms that need to seed local state from a react-query result
38
+ * but **must not** reset that state when the query refetches in the
39
+ * background. The canonical use case is the healthcheck editor: a realtime
40
+ * `HEALTH_CHECK_RUN_COMPLETED` signal invalidates the configuration query
41
+ * on every run, which would otherwise wipe the user's in-progress edits
42
+ * via a naive `useEffect([data], () => setState(data))`.
43
+ *
44
+ * Behaviour:
45
+ * - Calls `onInit(value)` the first time `value` is defined for a given
46
+ * `key`.
47
+ * - **Does NOT** call it again if `value` changes but `key` stays the
48
+ * same. Background refetches keep the same key (= the same record's
49
+ * primary id) and therefore don't re-run the initialiser.
50
+ * - **Does** call it again when `key` changes — e.g. when the user
51
+ * navigates to a different record without unmounting the page.
52
+ * - Skips initialisation entirely while either `value` or `key` is
53
+ * `undefined`/`null`.
54
+ *
55
+ * `onInit` is read from a ref so callers can safely pass a fresh closure
56
+ * each render without re-firing the effect.
57
+ *
58
+ * @example
59
+ * useInitOnceForKey(existingConfig, existingConfig?.id, (config) => {
60
+ * setFormState({
61
+ * name: config.name,
62
+ * collectors: config.collectors ?? [],
63
+ * });
64
+ * });
65
+ */
66
+ export function useInitOnceForKey<T>(
67
+ value: T | undefined | null,
68
+ key: string | number | null | undefined,
69
+ onInit: (value: T) => void,
70
+ ): void {
71
+ const initialisedKeyRef = useRef<string | number | null | undefined>(undefined);
72
+ const onInitRef = useRef(onInit);
73
+
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]);
87
+ }
package/src/index.ts CHANGED
@@ -63,3 +63,9 @@ export * from "./components/MetricTile";
63
63
  export * from "./components/Sheet";
64
64
  export * from "./components/Popover";
65
65
  export * from "./hooks/useIsMobile";
66
+ export * from "./hooks/useInitOnceForKey";
67
+ export * from "./components/ListEmptyState";
68
+ export * from "./components/QueryErrorState";
69
+ export * from "./components/Skeleton";
70
+ export * from "./components/ResponsiveTable";
71
+ export * from "./utils/toastTemplates";
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it, mock, beforeEach } from "bun:test";
2
+ import {
3
+ toastError,
4
+ toastSuccess,
5
+ type ToastApi,
6
+ } from "./toastTemplates";
7
+
8
+ const makeToastMock = (): ToastApi => ({
9
+ show: mock(),
10
+ success: mock(),
11
+ error: mock(),
12
+ warning: mock(),
13
+ info: mock(),
14
+ });
15
+
16
+ describe("toastSuccess", () => {
17
+ let toast: ToastApi;
18
+
19
+ beforeEach(() => {
20
+ toast = makeToastMock();
21
+ });
22
+
23
+ it("passes the action through to toast.success", () => {
24
+ toastSuccess(toast, "Check created");
25
+
26
+ expect(toast.success).toHaveBeenCalledTimes(1);
27
+ expect(toast.success).toHaveBeenCalledWith("Check created");
28
+ });
29
+ });
30
+
31
+ describe("toastError", () => {
32
+ let toast: ToastApi;
33
+
34
+ beforeEach(() => {
35
+ toast = makeToastMock();
36
+ });
37
+
38
+ it("prefixes the action and includes the extracted error message", () => {
39
+ toastError(toast, "Failed to create check", new Error("Backend unreachable"));
40
+
41
+ expect(toast.error).toHaveBeenCalledTimes(1);
42
+ expect(toast.error).toHaveBeenCalledWith(
43
+ "Failed to create check: Backend unreachable",
44
+ );
45
+ });
46
+
47
+ it("uses the fallback message when error is not an Error or string", () => {
48
+ toastError(toast, "Failed to delete check", null);
49
+
50
+ expect(toast.error).toHaveBeenCalledWith(
51
+ "Failed to delete check: An error occurred",
52
+ );
53
+ });
54
+
55
+ it("accepts a string error and includes it verbatim", () => {
56
+ toastError(toast, "Save failed", "validation: bad payload");
57
+
58
+ expect(toast.error).toHaveBeenCalledWith(
59
+ "Save failed: validation: bad payload",
60
+ );
61
+ });
62
+
63
+ it("truncates the message to 100 characters with an ellipsis", () => {
64
+ const longMessage = "x".repeat(200);
65
+ toastError(toast, "Failed", new Error(longMessage));
66
+
67
+ const mockedError = toast.error as ReturnType<typeof mock>;
68
+ const call = mockedError.mock.calls[0];
69
+ const arg = call[0];
70
+ expect(typeof arg).toBe("string");
71
+ if (typeof arg !== "string") throw new Error("expected string toast message");
72
+ expect(arg.length).toBe(100);
73
+ expect(arg.endsWith("…")).toBe(true);
74
+ });
75
+
76
+ it("leaves messages at or under 100 characters intact", () => {
77
+ const shortMessage = "boom";
78
+ toastError(toast, "Failed", new Error(shortMessage));
79
+
80
+ expect(toast.error).toHaveBeenCalledWith("Failed: boom");
81
+ });
82
+ });
@@ -0,0 +1,47 @@
1
+ import { extractErrorMessage } from "@checkstack/common";
2
+ import type { useToast } from "../components/ToastProvider";
3
+
4
+ /**
5
+ * Shape of the toast API exposed by {@link useToast}. Re-derived from
6
+ * the hook's return type so the helpers stay in lock-step with any
7
+ * future additions to the toast surface.
8
+ */
9
+ export type ToastApi = ReturnType<typeof useToast>;
10
+
11
+ const MAX_TOAST_MESSAGE_LENGTH = 100;
12
+
13
+ /**
14
+ * Truncate `message` to `MAX_TOAST_MESSAGE_LENGTH` characters, replacing
15
+ * the tail with an ellipsis. Used to keep error toasts readable when
16
+ * upstream payloads carry verbose stack traces.
17
+ */
18
+ function truncate(message: string): string {
19
+ if (message.length <= MAX_TOAST_MESSAGE_LENGTH) return message;
20
+ return `${message.slice(0, MAX_TOAST_MESSAGE_LENGTH - 1)}…`;
21
+ }
22
+
23
+ /**
24
+ * toastSuccess - shorthand for `toast.success(action)`.
25
+ *
26
+ * Use a verb-phrase for `action` so the toast reads naturally, e.g.
27
+ * `"Check created"`, `"Configuration saved"`.
28
+ */
29
+ export function toastSuccess(toast: ToastApi, action: string): void {
30
+ toast.success(action);
31
+ }
32
+
33
+ /**
34
+ * toastError - render an error toast in the canonical
35
+ * `"{action}: {message}"` format, truncated to 100 characters.
36
+ *
37
+ * Funnels the unknown `error` through {@link extractErrorMessage} so
38
+ * callers don't have to narrow at every call site.
39
+ */
40
+ export function toastError(
41
+ toast: ToastApi,
42
+ action: string,
43
+ error: unknown,
44
+ ): void {
45
+ const message = extractErrorMessage(error);
46
+ toast.error(truncate(`${action}: ${message}`));
47
+ }
@@ -0,0 +1,48 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Plus, ServerCog } from "lucide-react";
3
+ import { Button } from "../src/components/Button";
4
+ import { ListEmptyState } from "../src/components/ListEmptyState";
5
+
6
+ const meta: Meta<typeof ListEmptyState> = {
7
+ title: "Components/Feedback/ListEmptyState",
8
+ component: ListEmptyState,
9
+ tags: ["autodocs"],
10
+ args: {
11
+ resource: "checks",
12
+ },
13
+ };
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof ListEmptyState>;
17
+
18
+ export const Default: Story = {};
19
+
20
+ export const WithDescription: Story = {
21
+ args: {
22
+ resource: "incidents",
23
+ description:
24
+ "Incidents are reported by operators and tracked through resolution updates.",
25
+ },
26
+ };
27
+
28
+ export const WithAction: Story = {
29
+ args: {
30
+ resource: "checks",
31
+ description:
32
+ "Create a health check to start monitoring an endpoint.",
33
+ actions: (
34
+ <Button>
35
+ <Plus className="h-4 w-4 mr-2" />
36
+ Create your first check
37
+ </Button>
38
+ ),
39
+ },
40
+ };
41
+
42
+ export const CustomIcon: Story = {
43
+ args: {
44
+ resource: "satellites",
45
+ description: "Satellites probe remote networks Checkstack can't reach directly.",
46
+ icon: <ServerCog className="h-10 w-10" />,
47
+ },
48
+ };
@@ -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,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 /> };
package/tsconfig.json CHANGED
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "extends": "@checkstack/tsconfig/frontend.json",
3
3
  "include": [
4
- "src"
4
+ "src",
5
+ "src/components/CodeEditor/generated/stdlib-types.json",
6
+ "scripts"
5
7
  ],
6
8
  "references": [
7
9
  {