@tai42/studio-sdk 0.12.0 → 0.13.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 (75) hide show
  1. package/dist/components/code-block.d.ts.map +1 -1
  2. package/dist/components/code-block.js +26 -13
  3. package/dist/components/code-block.js.map +1 -1
  4. package/dist/components/components.css +20 -0
  5. package/dist/components/copy-field.d.ts.map +1 -1
  6. package/dist/components/copy-field.js +8 -73
  7. package/dist/components/copy-field.js.map +1 -1
  8. package/dist/components/date-range-picker.d.ts +73 -0
  9. package/dist/components/date-range-picker.d.ts.map +1 -0
  10. package/dist/components/date-range-picker.js +146 -0
  11. package/dist/components/date-range-picker.js.map +1 -0
  12. package/dist/components/dialog.d.ts +19 -1
  13. package/dist/components/dialog.d.ts.map +1 -1
  14. package/dist/components/dialog.js +16 -13
  15. package/dist/components/dialog.js.map +1 -1
  16. package/dist/components/dirty-guard.d.ts +28 -0
  17. package/dist/components/dirty-guard.d.ts.map +1 -0
  18. package/dist/components/dirty-guard.js +93 -0
  19. package/dist/components/dirty-guard.js.map +1 -0
  20. package/dist/components/extension-picker.d.ts.map +1 -1
  21. package/dist/components/extension-picker.js +7 -0
  22. package/dist/components/extension-picker.js.map +1 -1
  23. package/dist/components/form-dialog.d.ts +45 -0
  24. package/dist/components/form-dialog.d.ts.map +1 -0
  25. package/dist/components/form-dialog.js +71 -0
  26. package/dist/components/form-dialog.js.map +1 -0
  27. package/dist/components/json-tree.d.ts +7 -1
  28. package/dist/components/json-tree.d.ts.map +1 -1
  29. package/dist/components/json-tree.js +300 -13
  30. package/dist/components/json-tree.js.map +1 -1
  31. package/dist/components/markdown.d.ts +60 -0
  32. package/dist/components/markdown.d.ts.map +1 -0
  33. package/dist/components/markdown.js +435 -0
  34. package/dist/components/markdown.js.map +1 -0
  35. package/dist/components/tabs.d.ts +3 -1
  36. package/dist/components/tabs.d.ts.map +1 -1
  37. package/dist/components/tabs.js +2 -2
  38. package/dist/components/tabs.js.map +1 -1
  39. package/dist/hooks/useApi.d.ts +56 -28
  40. package/dist/hooks/useApi.d.ts.map +1 -1
  41. package/dist/hooks/useClipboardCopy.d.ts +26 -0
  42. package/dist/hooks/useClipboardCopy.d.ts.map +1 -0
  43. package/dist/hooks/useClipboardCopy.js +72 -0
  44. package/dist/hooks/useClipboardCopy.js.map +1 -0
  45. package/dist/index.d.ts +9 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +5 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/navigation/context.d.ts +13 -1
  50. package/dist/navigation/context.d.ts.map +1 -1
  51. package/dist/navigation/context.js +205 -7
  52. package/dist/navigation/context.js.map +1 -1
  53. package/dist/navigation/index.d.ts +2 -2
  54. package/dist/navigation/index.d.ts.map +1 -1
  55. package/dist/navigation/index.js +1 -1
  56. package/dist/navigation/index.js.map +1 -1
  57. package/dist/navigation/types.d.ts +11 -0
  58. package/dist/navigation/types.d.ts.map +1 -1
  59. package/package.json +2 -2
  60. package/src/components/code-block.tsx +72 -9
  61. package/src/components/components.css +20 -0
  62. package/src/components/copy-field.tsx +25 -81
  63. package/src/components/date-range-picker.tsx +256 -0
  64. package/src/components/dialog.tsx +59 -18
  65. package/src/components/dirty-guard.tsx +153 -0
  66. package/src/components/extension-picker.tsx +12 -0
  67. package/src/components/form-dialog.tsx +133 -0
  68. package/src/components/json-tree.tsx +477 -42
  69. package/src/components/markdown.tsx +552 -0
  70. package/src/components/tabs.tsx +10 -1
  71. package/src/hooks/useClipboardCopy.ts +105 -0
  72. package/src/index.ts +18 -0
  73. package/src/navigation/context.tsx +245 -7
  74. package/src/navigation/index.ts +2 -0
  75. package/src/navigation/types.ts +12 -0
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Unsaved-changes guard for tabbed editors. `Tabs` unmount inactive panels, so a
4
+ * tab switch tears an editor down and drops its edits. One registry closes the gap:
5
+ * editors report dirtiness via {@link useRegisterDirty}; {@link GuardedTabs} holds a
6
+ * switch away from a dirty editor behind a confirm; the same registry arms
7
+ * {@link useNavigationGuard}, so a route navigation confirms through the SAME dialog
8
+ * and a full-page unload raises the native prompt. The dialog is one shared
9
+ * {@link ConfirmDialog} driven by a pending-promise resolver, so the guard's
10
+ * `Promise<boolean>` handler and the tab switch both settle against it.
11
+ */
12
+ import { createContext, useCallback, useContext, useEffect, useId, useRef, useState, } from 'react';
13
+ import { ConfirmDialog } from './confirm-dialog';
14
+ import { Tabs } from './tabs';
15
+ import { useNavigationGuard } from '../navigation';
16
+ const DirtyGuardContext = createContext(null);
17
+ function DirtyGuardProvider({ children }) {
18
+ const dirtyIds = useRef(new Set());
19
+ // Mirror the registry's non-empty state into React so the navigation guard's
20
+ // `when` re-arms as editors go dirty/clean.
21
+ const [anyDirty, setAnyDirty] = useState(false);
22
+ const [open, setOpen] = useState(false);
23
+ const resolverRef = useRef(null);
24
+ const setDirty = useCallback((id, dirty) => {
25
+ if (dirty)
26
+ dirtyIds.current.add(id);
27
+ else
28
+ dirtyIds.current.delete(id);
29
+ setAnyDirty(dirtyIds.current.size > 0);
30
+ }, []);
31
+ const confirmLeaveIfDirty = useCallback(() => {
32
+ if (dirtyIds.current.size === 0)
33
+ return Promise.resolve(true);
34
+ return new Promise((resolve) => {
35
+ // Settle a still-pending confirm as "stay" before it is orphaned by the new one.
36
+ resolverRef.current?.(false);
37
+ resolverRef.current = resolve;
38
+ setOpen(true);
39
+ });
40
+ }, []);
41
+ const settle = useCallback((ok) => {
42
+ setOpen(false);
43
+ const resolve = resolverRef.current;
44
+ resolverRef.current = null;
45
+ resolve?.(ok);
46
+ }, []);
47
+ // The route navigation guard is armed from the SAME registry, so it confirms
48
+ // through this dialog and any armed guard raises the native unload prompt.
49
+ useNavigationGuard(anyDirty, confirmLeaveIfDirty);
50
+ return (_jsxs(DirtyGuardContext.Provider, { value: { setDirty, confirmLeaveIfDirty }, children: [children, open ? (_jsx(ConfirmDialog, { title: "Discard unsaved changes?", confirmLabel: "Discard changes", pendingLabel: "Discarding", confirmVariant: "danger", isPending: false, onConfirm: () => {
51
+ settle(true);
52
+ }, onClose: () => {
53
+ settle(false);
54
+ }, children: _jsx("p", { style: { margin: 0 }, children: "This editor has unsaved changes. Leaving now discards them." }) })) : null] }));
55
+ }
56
+ /**
57
+ * Report an editor's dirty state to the enclosing {@link GuardedTabs}. A no-op
58
+ * outside a provider (an editor rendered standalone, e.g. in isolation tests) so
59
+ * the hook is always safe to call.
60
+ */
61
+ export function useRegisterDirty(dirty) {
62
+ const ctx = useContext(DirtyGuardContext);
63
+ const id = useId();
64
+ useEffect(() => {
65
+ ctx?.setDirty(id, dirty);
66
+ }, [ctx, id, dirty]);
67
+ // Clear the registration on unmount (a tab switch unmounts the editor) so a
68
+ // torn-down editor never leaves the guard armed.
69
+ useEffect(() => () => ctx?.setDirty(id, false), [ctx, id]);
70
+ }
71
+ function GuardedTabsInner({ items, defaultValue, }) {
72
+ const ctx = useContext(DirtyGuardContext);
73
+ const [active, setActive] = useState(defaultValue ?? items[0]?.value);
74
+ // The tab set is dynamic (plugin/capability-gated tabs). If the active value is
75
+ // no longer present, fall back to the first tab rather than render no panel.
76
+ const effective = items.some((item) => item.value === active) ? active : items[0]?.value;
77
+ const onValueChange = (next) => {
78
+ void ctx?.confirmLeaveIfDirty().then((ok) => {
79
+ if (ok)
80
+ setActive(next);
81
+ });
82
+ };
83
+ return _jsx(Tabs, { items: items, value: effective, onValueChange: onValueChange });
84
+ }
85
+ /**
86
+ * A drop-in replacement for {@link Tabs} that guards a switch away from a dirty
87
+ * editor. Wraps the tabs in a {@link DirtyGuardProvider}, so an editor rendered in
88
+ * any panel can arm the guard through {@link useRegisterDirty}.
89
+ */
90
+ export function GuardedTabs({ items, defaultValue, }) {
91
+ return (_jsx(DirtyGuardProvider, { children: _jsx(GuardedTabsInner, { items: items, defaultValue: defaultValue }) }));
92
+ }
93
+ //# sourceMappingURL=dirty-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dirty-guard.js","sourceRoot":"","sources":["../../src/components/dirty-guard.tsx"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"extension-picker.d.ts","sourceRoot":"","sources":["../../src/components/extension-picker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAMnD,MAAM,WAAW,oBAAoB;IACnC,iFAAiF;IACjF,QAAQ,CAAC,SAAS,EAAE,SAAS,SAAS,EAAE,CAAC;IACzC,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAA6B,GAC9B,EAAE,oBAAoB,+BAmDtB"}
1
+ {"version":3,"file":"extension-picker.d.ts","sourceRoot":"","sources":["../../src/components/extension-picker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAOnD,MAAM,WAAW,oBAAoB;IACnC,iFAAiF;IACjF,QAAQ,CAAC,SAAS,EAAE,SAAS,SAAS,EAAE,CAAC;IACzC,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAA6B,GAC9B,EAAE,oBAAoB,+BA8DtB"}
@@ -2,9 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Badge } from './badge';
3
3
  import { Checkbox } from './checkbox';
4
4
  import { groupByKind, kindVariant } from './extension-grouping';
5
+ import { EmptyState } from './primitives';
5
6
  export function ExtensionPicker({ available, value, onChange, disabled, idPrefix = 'extension-picker', }) {
6
7
  const groups = groupByKind(available);
7
8
  const selected = new Set(value);
9
+ // An empty catalog renders the shared EmptyState HERE — inside the picker — so
10
+ // every call site inherits it rather than each guarding an empty checklist (a
11
+ // bare div that reads as broken). No provider has published an extension yet.
12
+ if (available.length === 0) {
13
+ return (_jsx("div", { "data-testid": idPrefix, className: "tai-stack", children: _jsx(EmptyState, { title: "No extensions are available." }) }));
14
+ }
8
15
  /**
9
16
  * Toggle a single extension. For a NON-STACKABLE kind (`backend`), checking one
10
17
  * first strips any other selected member of that kind before appending, so the
@@ -1 +1 @@
1
- {"version":3,"file":"extension-picker.js","sourceRoot":"","sources":["../../src/components/extension-picker.tsx"],"names":[],"mappings":";AAwBA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAahE,MAAM,UAAU,eAAe,CAAC,EAC9B,SAAS,EACT,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,GAAG,kBAAkB,GACR;IACrB,MAAM,MAAM,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAEhC;;;;;OAKG;IACH,MAAM,MAAM,GAAG,CAAC,SAAoB,EAAE,YAAqB,EAAE,OAAgB,EAAQ,EAAE;QACrF,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YAC1D,OAAO;QACT,CAAC;QACD,IAAI,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC;YAAE,OAAO;QACzC,MAAM,YAAY,GAAG,YAAY;YAC/B,CAAC,CAAC,IAAI,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAChF,CAAC,CAAC,SAAS,CAAC;QACd,MAAM,IAAI,GACR,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5F,QAAQ,CAAC,CAAC,GAAG,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC,CAAC,CAAC;IAEF,OAAO,CACL,6BAAkB,QAAQ,EAAE,SAAS,EAAC,WAAW,YAC9C,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACrB,eAAsB,SAAS,EAAC,uBAAuB,aAGrD,eAAK,SAAS,EAAC,SAAS,aACtB,KAAC,KAAK,IAAC,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,YAAG,KAAK,CAAC,IAAI,GAAS,EAC5D,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,WAAW,gCAAuB,CAAC,CAAC,CAAC,IAAI,IAC3E,EACN,cAAK,SAAS,EAAC,SAAS,YACrB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAChC,KAAC,QAAQ,IAEP,KAAK,EAAE,SAAS,CAAC,IAAI,EACrB,OAAO,EAAE,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,EACrC,QAAQ,EAAE,QAAQ,EAClB,eAAe,EAAE,CAAC,OAAO,EAAE,EAAE;4BAC3B,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;wBACjD,CAAC,IANI,SAAS,CAAC,IAAI,CAOnB,CACH,CAAC,GACE,KAnBE,KAAK,CAAC,IAAI,CAoBd,CACP,CAAC,GACE,CACP,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"extension-picker.js","sourceRoot":"","sources":["../../src/components/extension-picker.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `FormDialog` — a modal whose body is a real `<form>`, so implicit submission
3
+ * gives Enter-in-a-field → submit. Composes {@link Dialog} and owns the submit
4
+ * lifecycle: a `type="submit"` default button, a busy state that disables it and
5
+ * shows a Spinner while the action runs, and a close-on-success-ONLY async contract.
6
+ *
7
+ * The contract: `onSubmit` may return a promise; the dialog closes (via `onClose`)
8
+ * only when it RESOLVES. A rejection or synchronous throw keeps the dialog open and
9
+ * renders the failure through `ErrorState`, so the operator can retry the filled fields.
10
+ *
11
+ * The caller owns mount/unmount: mount only while active, and any close gesture
12
+ * (Cancel, Escape, overlay) calls `onClose`. `submitDisabled` blocks submission by
13
+ * click AND by Enter — a disabled default button is not an implicit-submission target.
14
+ */
15
+ import { type ReactNode } from 'react';
16
+ export interface FormDialogProps {
17
+ readonly title: string;
18
+ readonly description?: string;
19
+ /** The submit button's resting label (e.g. `Create scope`). */
20
+ readonly submitLabel: string;
21
+ /** The Spinner's accessible label while the action runs (e.g. `Creating`). */
22
+ readonly pendingLabel: string;
23
+ /**
24
+ * The action the form performs. A returned promise is awaited: the dialog
25
+ * closes only when it RESOLVES; a rejection (or a synchronous throw) keeps the
26
+ * dialog open and renders the failure loudly. A `void` return closes the
27
+ * dialog synchronously.
28
+ */
29
+ readonly onSubmit: () => void | Promise<void>;
30
+ /**
31
+ * Called on a close gesture (Cancel, Escape, overlay) and on a successful
32
+ * submit — at most once per dialog lifecycle. A close landing mid-submit wins:
33
+ * the later resolution does not fire a second `onClose`.
34
+ */
35
+ readonly onClose: () => void;
36
+ /** Blocks submission by click and by Enter while incomplete (a disabled default button is not an implicit-submission target). */
37
+ readonly submitDisabled?: boolean;
38
+ readonly submitVariant?: 'primary' | 'danger';
39
+ /** The Cancel button's label. */
40
+ readonly cancelLabel?: string;
41
+ /** The form body — the caller's fields. */
42
+ readonly children: ReactNode;
43
+ }
44
+ export declare function FormDialog({ title, description, submitLabel, pendingLabel, onSubmit, onClose, submitDisabled, submitVariant, cancelLabel, children, }: FormDialogProps): ReactNode;
45
+ //# sourceMappingURL=form-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-dialog.d.ts","sourceRoot":"","sources":["../../src/components/form-dialog.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAA+B,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;AAMzF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAC7B,iIAAiI;IACjI,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC9C,iCAAiC;IACjC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;CAC9B;AAED,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,EACX,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,cAAsB,EACtB,aAAyB,EACzB,WAAsB,EACtB,QAAQ,GACT,EAAE,eAAe,GAAG,SAAS,CAwE7B"}
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * `FormDialog` — a modal whose body is a real `<form>`, so implicit submission
4
+ * gives Enter-in-a-field → submit. Composes {@link Dialog} and owns the submit
5
+ * lifecycle: a `type="submit"` default button, a busy state that disables it and
6
+ * shows a Spinner while the action runs, and a close-on-success-ONLY async contract.
7
+ *
8
+ * The contract: `onSubmit` may return a promise; the dialog closes (via `onClose`)
9
+ * only when it RESOLVES. A rejection or synchronous throw keeps the dialog open and
10
+ * renders the failure through `ErrorState`, so the operator can retry the filled fields.
11
+ *
12
+ * The caller owns mount/unmount: mount only while active, and any close gesture
13
+ * (Cancel, Escape, overlay) calls `onClose`. `submitDisabled` blocks submission by
14
+ * click AND by Enter — a disabled default button is not an implicit-submission target.
15
+ */
16
+ import { useEffect, useRef, useState } from 'react';
17
+ import { Dialog } from './dialog';
18
+ import { Button, ErrorState, Spinner } from './primitives';
19
+ import { errorMessage } from '../errors';
20
+ export function FormDialog({ title, description, submitLabel, pendingLabel, onSubmit, onClose, submitDisabled = false, submitVariant = 'primary', cancelLabel = 'Cancel', children, }) {
21
+ const [isPending, setIsPending] = useState(false);
22
+ const [submitError, setSubmitError] = useState(null);
23
+ // Latches on the first close gesture so `onClose` fires at most once per lifecycle;
24
+ // a close mid-submit wins, and the later resolution must not close again.
25
+ const closedRef = useRef(false);
26
+ // Guards post-await setters from touching state after a mid-submit close unmounts.
27
+ const mountedRef = useRef(true);
28
+ useEffect(() => {
29
+ mountedRef.current = true;
30
+ return () => {
31
+ mountedRef.current = false;
32
+ };
33
+ }, []);
34
+ function close() {
35
+ if (closedRef.current)
36
+ return;
37
+ closedRef.current = true;
38
+ onClose();
39
+ }
40
+ function handleSubmit(event) {
41
+ // SPA: the form never navigates, so the default GET is prevented.
42
+ event.preventDefault();
43
+ // Also covers a programmatic `requestSubmit` and a second Enter mid-flight, so the
44
+ // action fires at most once per run.
45
+ if (isPending || submitDisabled)
46
+ return;
47
+ setSubmitError(null);
48
+ setIsPending(true);
49
+ // `Promise.resolve().then(onSubmit)` routes a synchronous throw into the rejection
50
+ // path too, so a sync failure surfaces like an async one.
51
+ Promise.resolve()
52
+ .then(() => onSubmit())
53
+ .then(() => {
54
+ // A mid-submit close already settled the lifecycle; don't close or set state again.
55
+ if (closedRef.current || !mountedRef.current)
56
+ return;
57
+ setIsPending(false);
58
+ close();
59
+ }, (reason) => {
60
+ if (closedRef.current || !mountedRef.current)
61
+ return;
62
+ setIsPending(false);
63
+ setSubmitError(reason);
64
+ });
65
+ }
66
+ return (_jsx(Dialog, { title: title, description: description, open: true, onOpenChange: (next) => {
67
+ if (!next)
68
+ close();
69
+ }, children: _jsxs("form", { onSubmit: handleSubmit, children: [children, submitError != null ? _jsx(ErrorState, { message: errorMessage(submitError) }) : null, _jsxs("div", { className: "tai-dialog-actions", children: [_jsx(Button, { type: "button", onClick: close, children: cancelLabel }), _jsxs(Button, { type: "submit", variant: submitVariant, disabled: isPending || submitDisabled, children: [isPending ? _jsx(Spinner, { label: pendingLabel }) : null, submitLabel] })] })] }) }));
70
+ }
71
+ //# sourceMappingURL=form-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-dialog.js","sourceRoot":"","sources":["../../src/components/form-dialog.tsx"],"names":[],"mappings":"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"}
@@ -1,8 +1,14 @@
1
+ import type { ReactElement } from 'react';
1
2
  export interface JsonTreeProps {
2
3
  readonly data: unknown;
4
+ /**
5
+ * Opts the whole tree out of the depth-guarded default: `true` expands every
6
+ * node (up to the depth cap), `false` collapses to the root. Omitted, the tree
7
+ * opens through {@link AUTO_EXPAND_DEPTH} and collapses everything below.
8
+ */
3
9
  readonly defaultExpanded?: boolean;
4
10
  /** The region's accessible name, applied only while the pane actually scrolls. */
5
11
  readonly label?: string;
6
12
  }
7
- export declare function JsonTree({ data, defaultExpanded, label }: JsonTreeProps): import("react").JSX.Element;
13
+ export declare function JsonTree({ data, defaultExpanded, label }: JsonTreeProps): ReactElement;
8
14
  //# sourceMappingURL=json-tree.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"json-tree.d.ts","sourceRoot":"","sources":["../../src/components/json-tree.tsx"],"names":[],"mappings":"AAmBA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,kFAAkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AA8FD,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,eAAsB,EAAE,KAAK,EAAE,EAAE,aAAa,+BAU9E"}
1
+ {"version":3,"file":"json-tree.d.ts","sourceRoot":"","sources":["../../src/components/json-tree.tsx"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAgD,YAAY,EAAE,MAAM,OAAO,CAAC;AAMxF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,kFAAkF;IAClF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AA2aD,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,EAAE,aAAa,GAAG,YAAY,CA2FtF"}
@@ -1,10 +1,65 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * `JsonTree` — a collapsible viewer for arbitrary JSON on the design system's
4
+ * terminal ground (`tai-code-block`). Objects and arrays are native
5
+ * `<details>`/`<summary>` disclosures; primitives render inline, tinted by their
6
+ * `tai-syntax-*` type class. All values render as React TEXT children, so markup in
7
+ * a payload is escaped — never an HTML sink.
8
+ *
9
+ * It mounts against guaranteed-large payloads (a trace span's whole input/output),
10
+ * so it is BOUNDED by construction:
11
+ *
12
+ * - Nodes deeper than {@link AUTO_EXPAND_DEPTH} start COLLAPSED, and a collapsed node
13
+ * renders no children. `defaultExpanded` overrides per call site.
14
+ * - Expand-all opens breadth-first only until {@link AUTO_EXPAND_NODE_BUDGET} nodes
15
+ * render and never past {@link MAX_AUTO_DEPTH}; a node beyond either opens on click.
16
+ * - A container over {@link PAGE_SIZE} children renders one page plus a "show more".
17
+ * - Copy-whole and copy-node write JSON to the clipboard; a refused write shows an alert.
18
+ *
19
+ * The scrolling pane IS the scrolling box, carrying the region attributes itself
20
+ * rather than nesting a `ScrollRegion` that would add a second scroller.
21
+ */
22
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
23
+ import { CheckIcon, CopyIcon, XCircleIcon } from './icons';
2
24
  import { useOverflowRegion } from './scroll-region';
25
+ import { COPIED_LABEL, COPIED_RESET_MS, useClipboardCopy } from '../hooks/useClipboardCopy';
3
26
  /** The region's name when the caller supplies none. */
4
27
  const DEFAULT_LABEL = 'JSON';
28
+ /** The deepest level opened by the guarded default; deeper nodes start collapsed. */
29
+ const AUTO_EXPAND_DEPTH = 1;
30
+ /**
31
+ * The deepest level EXPAND-ALL reaches. Past it a node opens only on an explicit
32
+ * click, so the one action that opens the whole tree still cannot force an
33
+ * unbounded-depth payload into the DOM in a single stroke.
34
+ */
35
+ const MAX_AUTO_DEPTH = 6;
36
+ /** Children rendered per page; the rest wait behind a "show more" control. */
37
+ const PAGE_SIZE = 100;
38
+ /**
39
+ * The most rendered nodes EXPAND-ALL may auto-open in one stroke. The depth cap and
40
+ * the per-page limit each bound ONE dimension, but a payload that is wide AND deep
41
+ * multiplies them — auto-opening every node up to the cap would lay out on the order
42
+ * of PAGE_SIZE^depth nodes and freeze the tab. Expand-all instead opens containers
43
+ * breadth-first only until this budget is spent; a node past it stays collapsed and
44
+ * opens on an explicit click, so a huge payload opens a bounded, usable subset.
45
+ */
46
+ const AUTO_EXPAND_NODE_BUDGET = 1500;
47
+ /** A copy control's resting face; whichever is exposed is also its accessible name. */
48
+ const COPY_LABEL = 'Copy';
49
+ /** Shown when the browser offers no clipboard at all (any non-secure context). */
50
+ const NO_CLIPBOARD = 'This browser will not write to the clipboard here. Select the value and copy it by hand.';
51
+ /** Shown when the write, or the value's serialization, is offered and refused. */
52
+ function copyFailed(reason) {
53
+ const detail = reason instanceof Error ? reason.message : String(reason);
54
+ return `Copy failed: ${detail}.`;
55
+ }
5
56
  function isRecord(value) {
6
57
  return typeof value === 'object' && value !== null && !Array.isArray(value);
7
58
  }
59
+ /** Whether a value is a disclosure (object or array) rather than a leaf. */
60
+ function isContainer(value) {
61
+ return Array.isArray(value) || isRecord(value);
62
+ }
8
63
  function primitiveText(value) {
9
64
  if (value === null)
10
65
  return 'null';
@@ -25,6 +80,100 @@ function primitiveText(value) {
25
80
  return '[object]';
26
81
  }
27
82
  }
83
+ /**
84
+ * `JSON.stringify` is TYPED to always return `string`, but a value it cannot
85
+ * represent (a function, a bare `undefined`) yields `undefined` at runtime. This
86
+ * boundary admits the absence the standard-library overload denies, so the guard
87
+ * below is a real check rather than a cast around one.
88
+ */
89
+ function stringifyJson(value) {
90
+ return JSON.stringify(value, null, 2);
91
+ }
92
+ /**
93
+ * The value's pretty JSON for the clipboard. A value JSON cannot represent (a
94
+ * function, a bare `undefined`) has no JSON, so its readable form is copied
95
+ * rather than an empty string. A value JSON REFUSES (a `bigint`) throws here, and
96
+ * the caller renders that as the same visible alert a blocked write is.
97
+ */
98
+ function serializeForCopy(value) {
99
+ return stringifyJson(value) ?? String(value);
100
+ }
101
+ /**
102
+ * A child's identity for the open-state map. Each segment is percent-encoded, so a
103
+ * key that itself contains the separator cannot collide with a different path.
104
+ */
105
+ function childPath(parent, name) {
106
+ const segment = encodeURIComponent(name);
107
+ return parent === '' ? segment : `${parent}/${segment}`;
108
+ }
109
+ function initialBaseline(defaultExpanded) {
110
+ if (defaultExpanded === true)
111
+ return 'expanded';
112
+ if (defaultExpanded === false)
113
+ return 'collapsed';
114
+ return 'depth';
115
+ }
116
+ /**
117
+ * The set of paths EXPAND-ALL auto-opens: a breadth-first sweep that opens each
118
+ * container in turn — spending its visible child count against a shared node budget
119
+ * and enqueuing that page's own container children — skipping any node whose page
120
+ * would overspend {@link AUTO_EXPAND_NODE_BUDGET} or that sits past {@link MAX_AUTO_DEPTH}.
121
+ * Only a bounded window of a huge payload is materialized, so this stays cheap even
122
+ * when the data is enormous; the paths it omits stay collapsed and open on a click.
123
+ */
124
+ function expandedOpenPaths(root) {
125
+ const open = new Set();
126
+ if (!isContainer(root))
127
+ return open;
128
+ let budget = AUTO_EXPAND_NODE_BUDGET;
129
+ const queue = [
130
+ { value: root, path: '', depth: 0 },
131
+ ];
132
+ while (queue.length > 0) {
133
+ const node = queue.shift();
134
+ if (node === undefined)
135
+ break;
136
+ if (node.depth >= MAX_AUTO_DEPTH)
137
+ continue;
138
+ const shown = Math.min(PAGE_SIZE, containerCount(node.value));
139
+ // Skip when the budget can't seat a whole page (a later, smaller node may still fit).
140
+ if (shown > budget)
141
+ continue;
142
+ budget -= shown;
143
+ open.add(node.path);
144
+ for (const [name, childValue] of sliceEntries(node.value, shown)) {
145
+ if (isContainer(childValue)) {
146
+ queue.push({ value: childValue, path: childPath(node.path, name), depth: node.depth + 1 });
147
+ }
148
+ }
149
+ }
150
+ return open;
151
+ }
152
+ /**
153
+ * Whether a node is open, from the baseline and the reader's own toggles. An
154
+ * explicit toggle always wins; otherwise the baseline decides — the depth-guarded
155
+ * default opens through {@link AUTO_EXPAND_DEPTH}, expand-all opens the budgeted
156
+ * breadth-first set, collapse-all opens nothing.
157
+ */
158
+ function computeOpen(baseline, overrides, expandedPaths, path, depth) {
159
+ const override = overrides.get(path);
160
+ if (override !== undefined)
161
+ return override;
162
+ if (baseline === 'expanded')
163
+ return expandedPaths?.has(path) ?? false;
164
+ if (baseline === 'collapsed')
165
+ return false;
166
+ return depth <= AUTO_EXPAND_DEPTH;
167
+ }
168
+ const JsonTreeContext = createContext(null);
169
+ /** The shared tree state; a subcomponent outside a `JsonTree` is a wiring error. */
170
+ function useJsonTreeContext() {
171
+ const context = useContext(JsonTreeContext);
172
+ if (context === null) {
173
+ throw new Error('JsonTree subcomponents must be rendered inside a JsonTree.');
174
+ }
175
+ return context;
176
+ }
28
177
  /**
29
178
  * One nesting level's indent: the child block insets and draws its own guide
30
179
  * rail. The depth is expressed by the DOM nesting, so each level repeats this
@@ -36,7 +185,24 @@ const childrenStyle = {
36
185
  borderLeft: '1px solid var(--tai-color-decor)',
37
186
  };
38
187
  /** A `<summary>` is a click target; browsers do not style it as one by default. */
39
- const summaryStyle = { cursor: 'pointer' };
188
+ const summaryStyle = {
189
+ display: 'flex',
190
+ alignItems: 'center',
191
+ gap: 'var(--tai-space-2)',
192
+ cursor: 'pointer',
193
+ };
194
+ /** The copy control sits at the far end of a disclosure's row. */
195
+ const nodeCopyStyle = { marginLeft: 'auto' };
196
+ /**
197
+ * The per-node copy button's geometry: borrows `tai-icon-btn` for the focus ring,
198
+ * muted ink and hover, then shrinks to the code rhythm so the row stays dense.
199
+ */
200
+ const nodeCopyButtonStyle = {
201
+ width: 'auto',
202
+ height: 'auto',
203
+ padding: 'var(--tai-space-1)',
204
+ };
205
+ const showMoreStyle = { marginTop: 'var(--tai-space-1)' };
40
206
  /** The syntax class for a primitive's type; anything outside JSON reads as muted. */
41
207
  function primitiveClass(value) {
42
208
  if (typeof value === 'string')
@@ -49,22 +215,143 @@ function primitiveClass(value) {
49
215
  return 'tai-syntax-null';
50
216
  return 'tai-muted';
51
217
  }
52
- function JsonNode({ name, value, defaultExpanded }) {
53
- if (Array.isArray(value) || isRecord(value)) {
54
- const entries = Array.isArray(value)
55
- ? value.map((item, index) => [String(index), item])
56
- : Object.entries(value);
57
- const summary = Array.isArray(value)
58
- ? `Array(${String(value.length)})`
59
- : `Object(${String(entries.length)})`;
60
- return (_jsxs("details", { open: defaultExpanded, children: [_jsxs("summary", { style: summaryStyle, children: [name !== undefined ? _jsxs("span", { className: "tai-syntax-key", children: [name, ": "] }) : null, _jsx("span", { className: "tai-muted", children: summary })] }), _jsx("div", { style: childrenStyle, children: entries.map(([childName, childValue]) => (_jsx(JsonNode, { name: childName, value: childValue, defaultExpanded: defaultExpanded }, childName))) })] }));
218
+ /**
219
+ * A copy control that writes its value to the clipboard and shows a transient
220
+ * confirmed state. The write itself lives on the tree context, so a refusal is
221
+ * reported ONCE — as the tree's shared alert — rather than inline at each of the
222
+ * many buttons a large payload carries.
223
+ */
224
+ function CopyButton({ value, label, variant }) {
225
+ const { copy } = useJsonTreeContext();
226
+ const [copied, setCopied] = useState(false);
227
+ const resetTimer = useRef(null);
228
+ // The write is async, so a copy can be in flight when the button unmounts (a dialog
229
+ // holding the tree closes on the same click); checked before the resolution sets state.
230
+ const mounted = useRef(true);
231
+ useEffect(() => {
232
+ mounted.current = true;
233
+ return () => {
234
+ mounted.current = false;
235
+ if (resetTimer.current !== null)
236
+ clearTimeout(resetTimer.current);
237
+ };
238
+ }, []);
239
+ const handleClick = (event) => {
240
+ // Inside a `<summary>` a click both toggles the disclosure natively and reaches
241
+ // the summary's own toggle handler; neither should fire for a copy.
242
+ event.preventDefault();
243
+ event.stopPropagation();
244
+ void copy(value).then((ok) => {
245
+ if (!ok || !mounted.current)
246
+ return;
247
+ setCopied(true);
248
+ if (resetTimer.current !== null)
249
+ clearTimeout(resetTimer.current);
250
+ resetTimer.current = setTimeout(() => {
251
+ if (mounted.current)
252
+ setCopied(false);
253
+ }, COPIED_RESET_MS);
254
+ });
255
+ };
256
+ if (variant === 'text') {
257
+ return (_jsxs("button", { type: "button", className: "tai-btn tai-btn-ghost", onClick: handleClick, children: [copied ? _jsx(CheckIcon, {}) : _jsx(CopyIcon, {}), copied ? COPIED_LABEL : label] }));
258
+ }
259
+ return (_jsx("button", { type: "button", className: "tai-icon-btn", style: { ...nodeCopyStyle, ...nodeCopyButtonStyle }, "aria-label": copied ? COPIED_LABEL : label, onClick: handleClick, children: copied ? _jsx(CheckIcon, {}) : _jsx(CopyIcon, {}) }));
260
+ }
261
+ /** A container's child count, without materializing its entries. */
262
+ function containerCount(value) {
263
+ if (Array.isArray(value))
264
+ return value.length;
265
+ if (isRecord(value))
266
+ return Object.keys(value).length;
267
+ throw new Error('containerCount expects an array or a record.');
268
+ }
269
+ /**
270
+ * The first `end` entries of a container, arrays keyed by index and objects by
271
+ * key. Only the visible window is materialized, so a million-element array's
272
+ * children never all exist at once.
273
+ */
274
+ function sliceEntries(value, end) {
275
+ if (Array.isArray(value)) {
276
+ return value.slice(0, end).map((item, index) => [String(index), item]);
277
+ }
278
+ // Slice the keys before reading values, so only the visible window is materialized.
279
+ if (isRecord(value))
280
+ return Object.keys(value)
281
+ .slice(0, end)
282
+ .map((key) => [key, value[key]]);
283
+ throw new Error('sliceEntries expects an array or a record.');
284
+ }
285
+ /** One container's summary text: its kind and its child count. */
286
+ function containerSummary(value, count) {
287
+ return Array.isArray(value) ? `Array(${String(count)})` : `Object(${String(count)})`;
288
+ }
289
+ /**
290
+ * The visible slice of a container's children, plus a "show more" control while
291
+ * any remain. Rendered ONLY when the node is open, so a collapsed container costs
292
+ * nothing and the page count follows how far the reader has opened it.
293
+ */
294
+ function NodeChildren({ value, count, depth, path, }) {
295
+ const { pageSize } = useJsonTreeContext();
296
+ const [visible, setVisible] = useState(pageSize);
297
+ const shownCount = Math.min(visible, count);
298
+ const shown = sliceEntries(value, shownCount);
299
+ const remaining = count - shownCount;
300
+ return (_jsxs("div", { style: childrenStyle, children: [shown.map(([childName, childValue]) => (_jsx(JsonNode, { name: childName, value: childValue, depth: depth + 1, path: childPath(path, childName) }, childName))), remaining > 0 ? (_jsxs("button", { type: "button", className: "tai-btn tai-btn-ghost", style: showMoreStyle, onClick: () => {
301
+ setVisible((current) => current + pageSize);
302
+ }, children: ["Show ", String(Math.min(remaining, pageSize)), " more"] })) : null] }));
303
+ }
304
+ function JsonNode({ name, value, depth, path }) {
305
+ const { isOpen, setOpen } = useJsonTreeContext();
306
+ if (isContainer(value)) {
307
+ const count = containerCount(value);
308
+ const open = isOpen(path, depth);
309
+ const summary = containerSummary(value, count);
310
+ const handleToggle = (event) => {
311
+ // The disclosure is state-driven: cancel the browser's own toggle so the two
312
+ // never diverge, then record the reader's intent.
313
+ event.preventDefault();
314
+ setOpen(path, !open);
315
+ };
316
+ return (_jsxs("details", { open: open, children: [_jsxs("summary", { style: summaryStyle, onClick: handleToggle, children: [name !== undefined ? _jsxs("span", { className: "tai-syntax-key", children: [name, ": "] }) : null, _jsx("span", { className: "tai-muted", children: summary }), depth > 0 ? (_jsx(CopyButton, { value: value, variant: "icon", label: name !== undefined ? `Copy ${name}` : COPY_LABEL })) : null] }), open ? _jsx(NodeChildren, { value: value, count: count, depth: depth, path: path }) : null] }));
61
317
  }
62
318
  return (_jsxs("div", { children: [name !== undefined ? _jsxs("span", { className: "tai-syntax-key", children: [name, ": "] }) : null, _jsx("span", { className: primitiveClass(value), children: primitiveText(value) })] }));
63
319
  }
64
- export function JsonTree({ data, defaultExpanded = true, label }) {
320
+ export function JsonTree({ data, defaultExpanded, label }) {
321
+ const [baseline, setBaseline] = useState(() => initialBaseline(defaultExpanded));
322
+ const [overrides, setOverrides] = useState(() => new Map());
323
+ const { error: copyError, announcement, copy: copyText, } = useClipboardCopy({
324
+ noClipboard: NO_CLIPBOARD,
325
+ writeFailed: copyFailed,
326
+ });
65
327
  // No consumer ref: the measurement's own callback ref is the only thing that
66
328
  // needs the pane, and this component publishes no `ref` prop.
67
329
  const region = useOverflowRegion(undefined, label ?? DEFAULT_LABEL);
68
- return (_jsx("div", { className: "tai-code-block", ...region, children: _jsx(JsonNode, { value: data, defaultExpanded: defaultExpanded }) }));
330
+ // Expand-all's open set is a function of the data alone, and it is consulted only
331
+ // while that baseline is active; deriving it lazily keeps a collapsed or
332
+ // depth-default tree from sweeping the payload at all.
333
+ const expandedPaths = useMemo(() => (baseline === 'expanded' ? expandedOpenPaths(data) : null), [baseline, data]);
334
+ const isOpen = useCallback((path, depth) => computeOpen(baseline, overrides, expandedPaths, path, depth), [baseline, overrides, expandedPaths]);
335
+ const setOpen = useCallback((path, open) => {
336
+ setOverrides((previous) => {
337
+ const next = new Map(previous);
338
+ next.set(path, open);
339
+ return next;
340
+ });
341
+ }, []);
342
+ // The value's JSON is produced lazily inside the write, so a value JSON refuses
343
+ // (a `bigint`) surfaces as the same visible alert a blocked write does.
344
+ const copy = useCallback((value) => copyText(() => serializeForCopy(value)), [copyText]);
345
+ const context = useMemo(() => ({ isOpen, setOpen, copy, pageSize: PAGE_SIZE }), [isOpen, setOpen, copy]);
346
+ const expandAll = () => {
347
+ setBaseline('expanded');
348
+ setOverrides(new Map());
349
+ };
350
+ const collapseAll = () => {
351
+ setBaseline('collapsed');
352
+ setOverrides(new Map());
353
+ };
354
+ const showToolbar = isContainer(data);
355
+ return (_jsx(JsonTreeContext.Provider, { value: context, children: _jsxs("div", { className: "tai-stack tai-stack-2", children: [showToolbar ? (_jsxs("div", { className: "tai-row", children: [_jsx("button", { type: "button", className: "tai-btn tai-btn-ghost", onClick: expandAll, children: "Expand all" }), _jsx("button", { type: "button", className: "tai-btn tai-btn-ghost", onClick: collapseAll, children: "Collapse all" }), _jsx(CopyButton, { value: data, variant: "text", label: COPY_LABEL }), _jsx("span", { "aria-live": "polite", className: "tai-visually-hidden", children: announcement })] })) : null, copyError !== undefined ? (_jsxs("span", { role: "alert", className: "tai-field-error", children: [_jsx(XCircleIcon, {}), copyError] })) : null, _jsx("div", { className: "tai-code-block", ...region, children: _jsx(JsonNode, { value: data, depth: 0, path: "" }) })] }) }));
69
356
  }
70
357
  //# sourceMappingURL=json-tree.js.map