@tai42/studio-sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +15 -0
- package/README.md +318 -0
- package/SECURITY.md +51 -0
- package/dist/components/badge.d.ts +13 -0
- package/dist/components/badge.d.ts.map +1 -0
- package/dist/components/badge.js +14 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/checkbox.d.ts +24 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.js +31 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/code-block.d.ts +7 -0
- package/dist/components/code-block.d.ts.map +1 -0
- package/dist/components/code-block.js +23 -0
- package/dist/components/code-block.js.map +1 -0
- package/dist/components/completion-input.d.ts +31 -0
- package/dist/components/completion-input.d.ts.map +1 -0
- package/dist/components/completion-input.js +61 -0
- package/dist/components/completion-input.js.map +1 -0
- package/dist/components/components.css +2212 -0
- package/dist/components/confirm-dialog.d.ts +40 -0
- package/dist/components/confirm-dialog.d.ts.map +1 -0
- package/dist/components/confirm-dialog.js +11 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/control-styles.d.ts +12 -0
- package/dist/components/control-styles.d.ts.map +1 -0
- package/dist/components/control-styles.js +14 -0
- package/dist/components/control-styles.js.map +1 -0
- package/dist/components/copy-field.d.ts +8 -0
- package/dist/components/copy-field.d.ts.map +1 -0
- package/dist/components/copy-field.js +106 -0
- package/dist/components/copy-field.js.map +1 -0
- package/dist/components/dialog.d.ts +21 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +25 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/drawer.d.ts +22 -0
- package/dist/components/drawer.d.ts.map +1 -0
- package/dist/components/drawer.js +32 -0
- package/dist/components/drawer.js.map +1 -0
- package/dist/components/error-boundary.d.ts +33 -0
- package/dist/components/error-boundary.d.ts.map +1 -0
- package/dist/components/error-boundary.js +36 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/extension-combo-builder.d.ts +70 -0
- package/dist/components/extension-combo-builder.d.ts.map +1 -0
- package/dist/components/extension-combo-builder.js +154 -0
- package/dist/components/extension-combo-builder.js.map +1 -0
- package/dist/components/extension-grouping.d.ts +65 -0
- package/dist/components/extension-grouping.d.ts.map +1 -0
- package/dist/components/extension-grouping.js +78 -0
- package/dist/components/extension-grouping.js.map +1 -0
- package/dist/components/extension-picker.d.ts +35 -0
- package/dist/components/extension-picker.d.ts.map +1 -0
- package/dist/components/extension-picker.js +31 -0
- package/dist/components/extension-picker.js.map +1 -0
- package/dist/components/external-link.d.ts +19 -0
- package/dist/components/external-link.d.ts.map +1 -0
- package/dist/components/external-link.js +12 -0
- package/dist/components/external-link.js.map +1 -0
- package/dist/components/field.d.ts +64 -0
- package/dist/components/field.d.ts.map +1 -0
- package/dist/components/field.js +56 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/fleet-report.d.ts +39 -0
- package/dist/components/fleet-report.d.ts.map +1 -0
- package/dist/components/fleet-report.js +50 -0
- package/dist/components/fleet-report.js.map +1 -0
- package/dist/components/fonts.css +22 -0
- package/dist/components/icons.d.ts +142 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/components/icons.js +139 -0
- package/dist/components/icons.js.map +1 -0
- package/dist/components/inputs.d.ts +52 -0
- package/dist/components/inputs.d.ts.map +1 -0
- package/dist/components/inputs.js +35 -0
- package/dist/components/inputs.js.map +1 -0
- package/dist/components/json-diff.d.ts +40 -0
- package/dist/components/json-diff.d.ts.map +1 -0
- package/dist/components/json-diff.js +119 -0
- package/dist/components/json-diff.js.map +1 -0
- package/dist/components/json-tree.d.ts +8 -0
- package/dist/components/json-tree.d.ts.map +1 -0
- package/dist/components/json-tree.js +70 -0
- package/dist/components/json-tree.js.map +1 -0
- package/dist/components/modal-focus.d.ts +15 -0
- package/dist/components/modal-focus.d.ts.map +1 -0
- package/dist/components/modal-focus.js +136 -0
- package/dist/components/modal-focus.js.map +1 -0
- package/dist/components/page-header.d.ts +56 -0
- package/dist/components/page-header.d.ts.map +1 -0
- package/dist/components/page-header.js +24 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/primitives.d.ts +186 -0
- package/dist/components/primitives.d.ts.map +1 -0
- package/dist/components/primitives.js +204 -0
- package/dist/components/primitives.js.map +1 -0
- package/dist/components/progress-bar.d.ts +29 -0
- package/dist/components/progress-bar.d.ts.map +1 -0
- package/dist/components/progress-bar.js +48 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/radio-group.d.ts +37 -0
- package/dist/components/radio-group.d.ts.map +1 -0
- package/dist/components/radio-group.js +56 -0
- package/dist/components/radio-group.js.map +1 -0
- package/dist/components/reveal-input.d.ts +13 -0
- package/dist/components/reveal-input.d.ts.map +1 -0
- package/dist/components/reveal-input.js +31 -0
- package/dist/components/reveal-input.js.map +1 -0
- package/dist/components/scroll-region.d.ts +85 -0
- package/dist/components/scroll-region.d.ts.map +1 -0
- package/dist/components/scroll-region.js +574 -0
- package/dist/components/scroll-region.js.map +1 -0
- package/dist/components/select.d.ts +53 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.js +33 -0
- package/dist/components/select.js.map +1 -0
- package/dist/components/table.d.ts +59 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.js +20 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/tabs.d.ts +15 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.js +13 -0
- package/dist/components/tabs.js.map +1 -0
- package/dist/components/tags.d.ts +46 -0
- package/dist/components/tags.d.ts.map +1 -0
- package/dist/components/tags.js +68 -0
- package/dist/components/tags.js.map +1 -0
- package/dist/components/tokens.css +392 -0
- package/dist/components/tokens.d.ts +15 -0
- package/dist/components/tokens.d.ts.map +1 -0
- package/dist/components/tokens.js +101 -0
- package/dist/components/tokens.js.map +1 -0
- package/dist/components/tool-picker.d.ts +24 -0
- package/dist/components/tool-picker.d.ts.map +1 -0
- package/dist/components/tool-picker.js +97 -0
- package/dist/components/tool-picker.js.map +1 -0
- package/dist/components/tooltip.d.ts +17 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.js +18 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/version-history-panel.d.ts +71 -0
- package/dist/components/version-history-panel.d.ts.map +1 -0
- package/dist/components/version-history-panel.js +148 -0
- package/dist/components/version-history-panel.js.map +1 -0
- package/dist/download.d.ts +7 -0
- package/dist/download.d.ts.map +1 -0
- package/dist/download.js +18 -0
- package/dist/download.js.map +1 -0
- package/dist/element-slot.d.ts +4 -0
- package/dist/element-slot.d.ts.map +1 -0
- package/dist/element-slot.js +20 -0
- package/dist/element-slot.js.map +1 -0
- package/dist/elicitation/ElicitationForm.d.ts +34 -0
- package/dist/elicitation/ElicitationForm.d.ts.map +1 -0
- package/dist/elicitation/ElicitationForm.js +24 -0
- package/dist/elicitation/ElicitationForm.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -0
- package/dist/extension-combos.d.ts +13 -0
- package/dist/extension-combos.d.ts.map +1 -0
- package/dist/extension-combos.js +9 -0
- package/dist/extension-combos.js.map +1 -0
- package/dist/hooks/useApi.d.ts +1222 -0
- package/dist/hooks/useApi.d.ts.map +1 -0
- package/dist/hooks/useApi.js +22 -0
- package/dist/hooks/useApi.js.map +1 -0
- package/dist/hooks/useAuth.d.ts +13 -0
- package/dist/hooks/useAuth.d.ts.map +1 -0
- package/dist/hooks/useAuth.js +53 -0
- package/dist/hooks/useAuth.js.map +1 -0
- package/dist/hooks/useBreakpoint.d.ts +10 -0
- package/dist/hooks/useBreakpoint.d.ts.map +1 -0
- package/dist/hooks/useBreakpoint.js +66 -0
- package/dist/hooks/useBreakpoint.js.map +1 -0
- package/dist/hooks/useCapabilities.d.ts +88 -0
- package/dist/hooks/useCapabilities.d.ts.map +1 -0
- package/dist/hooks/useCapabilities.js +203 -0
- package/dist/hooks/useCapabilities.js.map +1 -0
- package/dist/hooks/useSse.d.ts +13 -0
- package/dist/hooks/useSse.d.ts.map +1 -0
- package/dist/hooks/useSse.js +298 -0
- package/dist/hooks/useSse.js.map +1 -0
- package/dist/hooks/useTheme.d.ts +20 -0
- package/dist/hooks/useTheme.d.ts.map +1 -0
- package/dist/hooks/useTheme.js +97 -0
- package/dist/hooks/useTheme.js.map +1 -0
- package/dist/hooks/useUnauthorized.d.ts +3 -0
- package/dist/hooks/useUnauthorized.d.ts.map +1 -0
- package/dist/hooks/useUnauthorized.js +18 -0
- package/dist/hooks/useUnauthorized.js.map +1 -0
- package/dist/host.d.ts +11 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +10 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/context.d.ts +25 -0
- package/dist/navigation/context.d.ts.map +1 -0
- package/dist/navigation/context.js +45 -0
- package/dist/navigation/context.js.map +1 -0
- package/dist/navigation/index.d.ts +4 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +2 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/navigation/types.d.ts +97 -0
- package/dist/navigation/types.d.ts.map +1 -0
- package/dist/navigation/types.js +16 -0
- package/dist/navigation/types.js.map +1 -0
- package/dist/plugin/host-state.d.ts +45 -0
- package/dist/plugin/host-state.d.ts.map +1 -0
- package/dist/plugin/host-state.js +62 -0
- package/dist/plugin/host-state.js.map +1 -0
- package/dist/plugin/registry.d.ts +35 -0
- package/dist/plugin/registry.d.ts.map +1 -0
- package/dist/plugin/registry.js +129 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/types.d.ts +165 -0
- package/dist/plugin/types.d.ts.map +1 -0
- package/dist/plugin/types.js +2 -0
- package/dist/plugin/types.js.map +1 -0
- package/dist/plugin/version.d.ts +34 -0
- package/dist/plugin/version.d.ts.map +1 -0
- package/dist/plugin/version.js +37 -0
- package/dist/plugin/version.js.map +1 -0
- package/dist/query-keys.d.ts +31 -0
- package/dist/query-keys.d.ts.map +1 -0
- package/dist/query-keys.js +31 -0
- package/dist/query-keys.js.map +1 -0
- package/dist/schema-editor/SchemaEditor.d.ts +43 -0
- package/dist/schema-editor/SchemaEditor.d.ts.map +1 -0
- package/dist/schema-editor/SchemaEditor.js +106 -0
- package/dist/schema-editor/SchemaEditor.js.map +1 -0
- package/dist/schema-editor/index.d.ts +10 -0
- package/dist/schema-editor/index.d.ts.map +1 -0
- package/dist/schema-editor/index.js +8 -0
- package/dist/schema-editor/index.js.map +1 -0
- package/dist/schema-editor/lint.d.ts +37 -0
- package/dist/schema-editor/lint.d.ts.map +1 -0
- package/dist/schema-editor/lint.js +49 -0
- package/dist/schema-editor/lint.js.map +1 -0
- package/dist/schema-form/SchemaForm.d.ts +70 -0
- package/dist/schema-form/SchemaForm.d.ts.map +1 -0
- package/dist/schema-form/SchemaForm.js +13 -0
- package/dist/schema-form/SchemaForm.js.map +1 -0
- package/dist/schema-form/array-field.d.ts +20 -0
- package/dist/schema-form/array-field.d.ts.map +1 -0
- package/dist/schema-form/array-field.js +39 -0
- package/dist/schema-form/array-field.js.map +1 -0
- package/dist/schema-form/classify.d.ts +87 -0
- package/dist/schema-form/classify.d.ts.map +1 -0
- package/dist/schema-form/classify.js +199 -0
- package/dist/schema-form/classify.js.map +1 -0
- package/dist/schema-form/context.d.ts +4 -0
- package/dist/schema-form/context.d.ts.map +1 -0
- package/dist/schema-form/context.js +14 -0
- package/dist/schema-form/context.js.map +1 -0
- package/dist/schema-form/default-value.d.ts +8 -0
- package/dist/schema-form/default-value.d.ts.map +1 -0
- package/dist/schema-form/default-value.js +64 -0
- package/dist/schema-form/default-value.js.map +1 -0
- package/dist/schema-form/enum-field.d.ts +19 -0
- package/dist/schema-form/enum-field.d.ts.map +1 -0
- package/dist/schema-form/enum-field.js +26 -0
- package/dist/schema-form/enum-field.js.map +1 -0
- package/dist/schema-form/field-group.d.ts +9 -0
- package/dist/schema-form/field-group.d.ts.map +1 -0
- package/dist/schema-form/field-group.js +32 -0
- package/dist/schema-form/field-group.js.map +1 -0
- package/dist/schema-form/field-node.d.ts +24 -0
- package/dist/schema-form/field-node.d.ts.map +1 -0
- package/dist/schema-form/field-node.js +51 -0
- package/dist/schema-form/field-node.js.map +1 -0
- package/dist/schema-form/index.d.ts +13 -0
- package/dist/schema-form/index.d.ts.map +1 -0
- package/dist/schema-form/index.js +11 -0
- package/dist/schema-form/index.js.map +1 -0
- package/dist/schema-form/media-field.d.ts +24 -0
- package/dist/schema-form/media-field.d.ts.map +1 -0
- package/dist/schema-form/media-field.js +169 -0
- package/dist/schema-form/media-field.js.map +1 -0
- package/dist/schema-form/media.d.ts +40 -0
- package/dist/schema-form/media.d.ts.map +1 -0
- package/dist/schema-form/media.js +65 -0
- package/dist/schema-form/media.js.map +1 -0
- package/dist/schema-form/object-fields.d.ts +20 -0
- package/dist/schema-form/object-fields.d.ts.map +1 -0
- package/dist/schema-form/object-fields.js +24 -0
- package/dist/schema-form/object-fields.js.map +1 -0
- package/dist/schema-form/resolve.d.ts +41 -0
- package/dist/schema-form/resolve.d.ts.map +1 -0
- package/dist/schema-form/resolve.js +102 -0
- package/dist/schema-form/resolve.js.map +1 -0
- package/dist/schema-form/string-field.d.ts +20 -0
- package/dist/schema-form/string-field.d.ts.map +1 -0
- package/dist/schema-form/string-field.js +68 -0
- package/dist/schema-form/string-field.js.map +1 -0
- package/dist/schema-form/styles.d.ts +18 -0
- package/dist/schema-form/styles.d.ts.map +1 -0
- package/dist/schema-form/styles.js +18 -0
- package/dist/schema-form/styles.js.map +1 -0
- package/dist/schema-form/types.d.ts +67 -0
- package/dist/schema-form/types.d.ts.map +1 -0
- package/dist/schema-form/types.js +10 -0
- package/dist/schema-form/types.js.map +1 -0
- package/dist/schema-form/union-field.d.ts +24 -0
- package/dist/schema-form/union-field.d.ts.map +1 -0
- package/dist/schema-form/union-field.js +55 -0
- package/dist/schema-form/union-field.js.map +1 -0
- package/dist/schema-form/union.d.ts +4 -0
- package/dist/schema-form/union.d.ts.map +1 -0
- package/dist/schema-form/union.js +68 -0
- package/dist/schema-form/union.js.map +1 -0
- package/dist/schema-form/unsupported-notice.d.ts +18 -0
- package/dist/schema-form/unsupported-notice.d.ts.map +1 -0
- package/dist/schema-form/unsupported-notice.js +8 -0
- package/dist/schema-form/unsupported-notice.js.map +1 -0
- package/dist/schema-form/validate.d.ts +18 -0
- package/dist/schema-form/validate.d.ts.map +1 -0
- package/dist/schema-form/validate.js +162 -0
- package/dist/schema-form/validate.js.map +1 -0
- package/dist/structured-output/StructuredOutput.d.ts +27 -0
- package/dist/structured-output/StructuredOutput.d.ts.map +1 -0
- package/dist/structured-output/StructuredOutput.js +41 -0
- package/dist/structured-output/StructuredOutput.js.map +1 -0
- package/dist/testing.d.ts +80 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +207 -0
- package/dist/testing.js.map +1 -0
- package/package.json +101 -0
- package/src/components/badge.tsx +31 -0
- package/src/components/checkbox.tsx +93 -0
- package/src/components/code-block.tsx +37 -0
- package/src/components/completion-input.tsx +137 -0
- package/src/components/components.css +2212 -0
- package/src/components/confirm-dialog.tsx +77 -0
- package/src/components/control-styles.ts +14 -0
- package/src/components/copy-field.tsx +178 -0
- package/src/components/dialog.tsx +76 -0
- package/src/components/drawer.tsx +83 -0
- package/src/components/error-boundary.tsx +56 -0
- package/src/components/extension-combo-builder.tsx +305 -0
- package/src/components/extension-grouping.ts +116 -0
- package/src/components/extension-picker.tsx +97 -0
- package/src/components/external-link.tsx +40 -0
- package/src/components/field.tsx +169 -0
- package/src/components/fleet-report.tsx +136 -0
- package/src/components/fonts.css +22 -0
- package/src/components/icons.tsx +504 -0
- package/src/components/inputs.tsx +125 -0
- package/src/components/json-diff.tsx +192 -0
- package/src/components/json-tree.tsx +129 -0
- package/src/components/modal-focus.ts +150 -0
- package/src/components/page-header.tsx +107 -0
- package/src/components/primitives.tsx +489 -0
- package/src/components/progress-bar.tsx +100 -0
- package/src/components/radio-group.tsx +160 -0
- package/src/components/reveal-input.tsx +87 -0
- package/src/components/scroll-region.tsx +703 -0
- package/src/components/select.tsx +149 -0
- package/src/components/table.tsx +94 -0
- package/src/components/tabs.tsx +51 -0
- package/src/components/tags.tsx +137 -0
- package/src/components/tokens.css +392 -0
- package/src/components/tokens.ts +102 -0
- package/src/components/tool-picker.tsx +175 -0
- package/src/components/tooltip.tsx +46 -0
- package/src/components/version-history-panel.tsx +407 -0
- package/src/css.d.ts +2 -0
- package/src/download.ts +17 -0
- package/src/element-slot.ts +22 -0
- package/src/elicitation/ElicitationForm.tsx +87 -0
- package/src/errors.ts +12 -0
- package/src/extension-combos.ts +18 -0
- package/src/hooks/useApi.ts +27 -0
- package/src/hooks/useAuth.ts +67 -0
- package/src/hooks/useBreakpoint.ts +78 -0
- package/src/hooks/useCapabilities.ts +242 -0
- package/src/hooks/useSse.ts +314 -0
- package/src/hooks/useTheme.ts +133 -0
- package/src/hooks/useUnauthorized.ts +20 -0
- package/src/host.ts +15 -0
- package/src/index.ts +267 -0
- package/src/navigation/context.tsx +90 -0
- package/src/navigation/index.ts +9 -0
- package/src/navigation/types.ts +90 -0
- package/src/plugin/host-state.ts +93 -0
- package/src/plugin/registry.ts +174 -0
- package/src/plugin/types.ts +178 -0
- package/src/plugin/version.ts +44 -0
- package/src/query-keys.ts +34 -0
- package/src/schema-editor/SchemaEditor.tsx +186 -0
- package/src/schema-editor/index.ts +9 -0
- package/src/schema-editor/lint.ts +70 -0
- package/src/schema-form/SchemaForm.tsx +107 -0
- package/src/schema-form/array-field.tsx +110 -0
- package/src/schema-form/classify.ts +302 -0
- package/src/schema-form/context.ts +17 -0
- package/src/schema-form/default-value.ts +65 -0
- package/src/schema-form/enum-field.tsx +65 -0
- package/src/schema-form/field-group.tsx +71 -0
- package/src/schema-form/field-node.tsx +161 -0
- package/src/schema-form/index.ts +12 -0
- package/src/schema-form/media-field.tsx +281 -0
- package/src/schema-form/media.ts +69 -0
- package/src/schema-form/object-fields.tsx +74 -0
- package/src/schema-form/resolve.ts +114 -0
- package/src/schema-form/string-field.tsx +161 -0
- package/src/schema-form/styles.ts +20 -0
- package/src/schema-form/types.ts +71 -0
- package/src/schema-form/union-field.tsx +122 -0
- package/src/schema-form/union.ts +78 -0
- package/src/schema-form/unsupported-notice.tsx +39 -0
- package/src/schema-form/validate.ts +183 -0
- package/src/structured-output/StructuredOutput.tsx +81 -0
- package/src/testing.ts +235 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Tooltip` — a design-system wrapper over Radix Tooltip (content gets role
|
|
3
|
+
* `tooltip`, shown on hover/focus, wired to the trigger by Radix). Each instance
|
|
4
|
+
* carries its own Provider so a single tooltip works without the caller mounting
|
|
5
|
+
* one, and `delayDuration` tunes that Provider's open delay.
|
|
6
|
+
*
|
|
7
|
+
* The bubble is the `tai-tooltip` surface; the arrow is the one paint the class
|
|
8
|
+
* layer cannot reach (Radix renders it as an inline SVG), so it takes the same
|
|
9
|
+
* raised-surface token the bubble's background resolves from.
|
|
10
|
+
*/
|
|
11
|
+
import * as RadixTooltip from '@radix-ui/react-tooltip';
|
|
12
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
13
|
+
|
|
14
|
+
import { assertSlotElement } from '../element-slot';
|
|
15
|
+
|
|
16
|
+
export interface TooltipProps {
|
|
17
|
+
readonly content: ReactNode;
|
|
18
|
+
/**
|
|
19
|
+
* The element the bubble describes and hangs off.
|
|
20
|
+
*
|
|
21
|
+
* It is a single ELEMENT, not any node: Radix clones its props onto it, so a
|
|
22
|
+
* string throws. The type admits a FRAGMENT — `<></>` is a `ReactElement` —
|
|
23
|
+
* which would render children carrying none of the hover/focus wiring nor the
|
|
24
|
+
* `aria-describedby` back to the bubble, so {@link assertSlotElement} rejects
|
|
25
|
+
* that one at runtime.
|
|
26
|
+
*/
|
|
27
|
+
readonly children: ReactElement;
|
|
28
|
+
readonly delayDuration?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function Tooltip({ content, children, delayDuration = 200 }: TooltipProps) {
|
|
32
|
+
assertSlotElement(children, 'Tooltip `children`');
|
|
33
|
+
return (
|
|
34
|
+
<RadixTooltip.Provider delayDuration={delayDuration}>
|
|
35
|
+
<RadixTooltip.Root>
|
|
36
|
+
<RadixTooltip.Trigger asChild>{children}</RadixTooltip.Trigger>
|
|
37
|
+
<RadixTooltip.Portal>
|
|
38
|
+
<RadixTooltip.Content sideOffset={4} className="tai-tooltip">
|
|
39
|
+
{content}
|
|
40
|
+
<RadixTooltip.Arrow style={{ fill: 'var(--tai-color-surface-raised)' }} />
|
|
41
|
+
</RadixTooltip.Content>
|
|
42
|
+
</RadixTooltip.Portal>
|
|
43
|
+
</RadixTooltip.Root>
|
|
44
|
+
</RadixTooltip.Provider>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `VersionHistoryPanel` — a GENERIC, kind-agnostic viewer for a versioned
|
|
3
|
+
* document's history. It knows nothing about presets, AC policies, or authored
|
|
4
|
+
* agents: it takes a plain versions list (`{version, body, is_current,
|
|
5
|
+
* created_at}`, optionally `tags`) plus a `onRollback` callback, renders the list
|
|
6
|
+
* (a "Current" status mark driven by `is_current`), a `JsonTree` view of the selected
|
|
7
|
+
* version's opaque body, a per-row Rollback action behind a confirm dialog, and a
|
|
8
|
+
* per-row Compare action that opens a structural `JsonDiff` of two versions' bodies.
|
|
9
|
+
* Each consumer (e.g. presets, AC policies) wires it to its own routes.
|
|
10
|
+
*
|
|
11
|
+
* Two optional affordances, inert unless supplied:
|
|
12
|
+
* - a Tags column appears when any entry carries `tags` OR `onEditTags` is given
|
|
13
|
+
* (so tag-less consumers see no empty column);
|
|
14
|
+
* - when `onEditTags` is provided, a per-row "Edit tags" action opens a small
|
|
15
|
+
* dialog seeded from the row's tags; SAVE calls the callback with the NEW list
|
|
16
|
+
* and the panel awaits it with INTERNAL pending/error state, rendering a
|
|
17
|
+
* rejection's message verbatim.
|
|
18
|
+
*
|
|
19
|
+
* SAFETY: `body` and tags are arbitrary server JSON, rendered through `JsonTree` /
|
|
20
|
+
* `Badge` (React escapes every value) — never an HTML sink.
|
|
21
|
+
*
|
|
22
|
+
* Geometry and ink come from the design-system classes. Two rules shape the table:
|
|
23
|
+
* a row's state is a MARK plus a label (`tai-status` + `CheckCircleIcon` /
|
|
24
|
+
* `PendingIcon`), never color alone, and the version number and timestamp are the
|
|
25
|
+
* machine voice (`tai-table-id` / `tai-mono`). The table itself sits in a
|
|
26
|
+
* `ScrollRegion` so a narrow viewport scrolls it instead of the page.
|
|
27
|
+
*/
|
|
28
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
29
|
+
|
|
30
|
+
import { errorMessage } from '../errors';
|
|
31
|
+
import { Dialog } from './dialog';
|
|
32
|
+
import { Field } from './field';
|
|
33
|
+
import { CheckCircleIcon, PendingIcon } from './icons';
|
|
34
|
+
import { JsonDiff } from './json-diff';
|
|
35
|
+
import { JsonTree } from './json-tree';
|
|
36
|
+
import { Button, Card, ErrorState, Spinner } from './primitives';
|
|
37
|
+
import { ScrollRegion } from './scroll-region';
|
|
38
|
+
import { Table, TBody, TD, TH, THead, TR } from './table';
|
|
39
|
+
import { TagChips, TagsInput } from './tags';
|
|
40
|
+
|
|
41
|
+
/** One version row — the kind-agnostic shape every consumer projects into. */
|
|
42
|
+
export interface VersionHistoryEntry {
|
|
43
|
+
readonly version: number;
|
|
44
|
+
readonly body: unknown;
|
|
45
|
+
readonly is_current: boolean;
|
|
46
|
+
readonly created_at: string;
|
|
47
|
+
/** The version's generic label list, surfaced as a Tags column when present. */
|
|
48
|
+
readonly tags?: readonly string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface VersionHistoryPanelProps {
|
|
52
|
+
readonly versions: readonly VersionHistoryEntry[];
|
|
53
|
+
/** Roll the active pointer to `version`. The consumer owns the mutation. */
|
|
54
|
+
readonly onRollback: (version: number) => void;
|
|
55
|
+
/** Fired when a version is selected for viewing (optional). */
|
|
56
|
+
readonly onView?: (version: number) => void;
|
|
57
|
+
/** The consumer's rollback mutation is in flight. */
|
|
58
|
+
readonly rollbackPending?: boolean;
|
|
59
|
+
/** A loud rollback failure message to surface in the confirm dialog. */
|
|
60
|
+
readonly rollbackError?: string;
|
|
61
|
+
/**
|
|
62
|
+
* A read-only history view: the version list and each version's body stay
|
|
63
|
+
* visible (history is a read surface), but EVERY mutating action is hidden —
|
|
64
|
+
* the per-row Rollback and, where `onEditTags` is supplied, Edit tags.
|
|
65
|
+
* Defaults to interactive.
|
|
66
|
+
*/
|
|
67
|
+
readonly readOnly?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Extra consequence copy appended to the rollback confirm dialog's description
|
|
70
|
+
* (announced to assistive tech via the dialog's `aria-describedby`). Consumers
|
|
71
|
+
* whose rollback has an immediate live effect (e.g. re-pointing access-control
|
|
72
|
+
* enforcement) pass a sentence stating so; omitted when there is nothing extra.
|
|
73
|
+
*/
|
|
74
|
+
readonly rollbackConfirmDescription?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Replace one version's tag annotation. When provided, a per-row "Edit tags"
|
|
77
|
+
* action opens a dialog seeded from the row's tags and SAVE calls this with the
|
|
78
|
+
* NEW list; the panel awaits the returned promise with internal pending/error
|
|
79
|
+
* state. Omitted ⇒ no edit affordance (tags stay read-only).
|
|
80
|
+
*/
|
|
81
|
+
readonly onEditTags?: (version: number, tags: string[]) => Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The initially-selected version: the current one, else the first listed. */
|
|
85
|
+
function initialSelection(versions: readonly VersionHistoryEntry[]): number | null {
|
|
86
|
+
const current = versions.find((entry) => entry.is_current);
|
|
87
|
+
return current?.version ?? versions[0]?.version ?? null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The per-version tag editor dialog. Seeds a draft from the row's tags and calls
|
|
92
|
+
* `onSave` on submit, awaiting it with LOCAL pending/error state: a rejection's
|
|
93
|
+
* message renders verbatim and the dialog stays open; success closes it.
|
|
94
|
+
*/
|
|
95
|
+
function EditTagsDialog({
|
|
96
|
+
version,
|
|
97
|
+
initialTags,
|
|
98
|
+
onSave,
|
|
99
|
+
onClose,
|
|
100
|
+
}: {
|
|
101
|
+
readonly version: number;
|
|
102
|
+
readonly initialTags: readonly string[];
|
|
103
|
+
readonly onSave: (version: number, tags: string[]) => Promise<void>;
|
|
104
|
+
readonly onClose: () => void;
|
|
105
|
+
}): ReactNode {
|
|
106
|
+
const [tags, setTags] = useState<string[]>([...initialTags]);
|
|
107
|
+
const [pending, setPending] = useState(false);
|
|
108
|
+
const [error, setError] = useState<string | undefined>(undefined);
|
|
109
|
+
|
|
110
|
+
const submit = (): void => {
|
|
111
|
+
setPending(true);
|
|
112
|
+
setError(undefined);
|
|
113
|
+
onSave(version, tags).then(
|
|
114
|
+
() => {
|
|
115
|
+
onClose();
|
|
116
|
+
},
|
|
117
|
+
(reason: unknown) => {
|
|
118
|
+
setError(errorMessage(reason));
|
|
119
|
+
setPending(false);
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Dialog
|
|
126
|
+
title={`Edit tags — version ${String(version)}`}
|
|
127
|
+
description="Version tags are labels on an immutable body; editing them rebinds nothing."
|
|
128
|
+
open
|
|
129
|
+
onOpenChange={(next) => {
|
|
130
|
+
if (!next && !pending) onClose();
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
<div className="tai-stack">
|
|
134
|
+
{/* The editor is named by its Field, like every other site: its draft input
|
|
135
|
+
claims the Field's control id, so the visible label IS the name. */}
|
|
136
|
+
<Field label="Tags" description="Labels on this version's immutable body.">
|
|
137
|
+
<TagsInput value={tags} onChange={setTags} disabled={pending} />
|
|
138
|
+
</Field>
|
|
139
|
+
{error !== undefined ? <ErrorState message={error} /> : null}
|
|
140
|
+
</div>
|
|
141
|
+
<div className="tai-dialog-actions">
|
|
142
|
+
<Button
|
|
143
|
+
onClick={() => {
|
|
144
|
+
onClose();
|
|
145
|
+
}}
|
|
146
|
+
disabled={pending}
|
|
147
|
+
>
|
|
148
|
+
Cancel
|
|
149
|
+
</Button>
|
|
150
|
+
<Button variant="primary" onClick={submit} disabled={pending}>
|
|
151
|
+
{pending ? <Spinner label="Saving tags" /> : null}
|
|
152
|
+
Save tags
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
</Dialog>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function VersionHistoryPanel({
|
|
160
|
+
versions,
|
|
161
|
+
onRollback,
|
|
162
|
+
onView,
|
|
163
|
+
rollbackPending,
|
|
164
|
+
rollbackError,
|
|
165
|
+
readOnly = false,
|
|
166
|
+
rollbackConfirmDescription,
|
|
167
|
+
onEditTags,
|
|
168
|
+
}: VersionHistoryPanelProps): ReactNode {
|
|
169
|
+
const [selected, setSelected] = useState<number | null>(() => initialSelection(versions));
|
|
170
|
+
const [confirming, setConfirming] = useState<number | null>(null);
|
|
171
|
+
// The armed Compare source (first click), and the open compare pair (second click).
|
|
172
|
+
const [compareFrom, setCompareFrom] = useState<number | null>(null);
|
|
173
|
+
const [comparePair, setComparePair] = useState<{ from: number; to: number } | null>(null);
|
|
174
|
+
const [editingTags, setEditingTags] = useState<number | null>(null);
|
|
175
|
+
|
|
176
|
+
// Close the confirm dialog on a SUCCESSFUL rollback: the consumer's mutation
|
|
177
|
+
// drives `rollbackPending` true→false, and success is that transition WITHOUT a
|
|
178
|
+
// `rollbackError`. We compare the prior `rollbackPending` to the current one via a
|
|
179
|
+
// ref so this fires exactly once on the true→false edge — never on mount (the ref
|
|
180
|
+
// seeds from the initial value, so no edge exists) and never on a failure (a set
|
|
181
|
+
// `rollbackError` keeps the dialog open showing the error).
|
|
182
|
+
const wasRollbackPending = useRef<boolean | undefined>(rollbackPending);
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
const settledFromPending = wasRollbackPending.current === true && rollbackPending !== true;
|
|
185
|
+
wasRollbackPending.current = rollbackPending;
|
|
186
|
+
if (settledFromPending && rollbackError === undefined) {
|
|
187
|
+
setConfirming(null);
|
|
188
|
+
}
|
|
189
|
+
}, [rollbackPending, rollbackError]);
|
|
190
|
+
|
|
191
|
+
const selectedEntry = versions.find((entry) => entry.version === selected);
|
|
192
|
+
const showTags =
|
|
193
|
+
onEditTags !== undefined || versions.some((entry) => (entry.tags ?? []).length > 0);
|
|
194
|
+
const canCompare = versions.length >= 2;
|
|
195
|
+
|
|
196
|
+
const view = (version: number): void => {
|
|
197
|
+
setSelected(version);
|
|
198
|
+
onView?.(version);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const compare = (version: number): void => {
|
|
202
|
+
if (compareFrom === null) {
|
|
203
|
+
setCompareFrom(version);
|
|
204
|
+
} else if (compareFrom === version) {
|
|
205
|
+
setCompareFrom(null);
|
|
206
|
+
} else {
|
|
207
|
+
setComparePair({ from: compareFrom, to: version });
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const closeCompare = (): void => {
|
|
212
|
+
setComparePair(null);
|
|
213
|
+
setCompareFrom(null);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const editingEntry = versions.find((entry) => entry.version === editingTags);
|
|
217
|
+
const fromBody = versions.find((entry) => entry.version === comparePair?.from)?.body;
|
|
218
|
+
const toBody = versions.find((entry) => entry.version === comparePair?.to)?.body;
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<div className="tai-stack">
|
|
222
|
+
<Card>
|
|
223
|
+
{/* The action column makes this table wider than a phone viewport; it scrolls
|
|
224
|
+
inside its own region rather than pushing the page sideways. */}
|
|
225
|
+
<ScrollRegion label="Version history">
|
|
226
|
+
<Table>
|
|
227
|
+
<THead>
|
|
228
|
+
<TR>
|
|
229
|
+
<TH numeric>Version</TH>
|
|
230
|
+
<TH>Created</TH>
|
|
231
|
+
<TH>Status</TH>
|
|
232
|
+
{showTags ? <TH>Tags</TH> : null}
|
|
233
|
+
<TH>Actions</TH>
|
|
234
|
+
</TR>
|
|
235
|
+
</THead>
|
|
236
|
+
<TBody>
|
|
237
|
+
{versions.map((entry) => (
|
|
238
|
+
<TR key={entry.version} data-testid={`version-row-${String(entry.version)}`}>
|
|
239
|
+
<TD className="tai-table-id" numeric>
|
|
240
|
+
{entry.version}
|
|
241
|
+
</TD>
|
|
242
|
+
<TD className="tai-mono">{entry.created_at}</TD>
|
|
243
|
+
<TD>
|
|
244
|
+
{entry.is_current ? (
|
|
245
|
+
<span className="tai-status tai-status-ok">
|
|
246
|
+
<CheckCircleIcon />
|
|
247
|
+
Current
|
|
248
|
+
</span>
|
|
249
|
+
) : (
|
|
250
|
+
<span className="tai-status tai-status-pending">
|
|
251
|
+
<PendingIcon />
|
|
252
|
+
Historical
|
|
253
|
+
</span>
|
|
254
|
+
)}
|
|
255
|
+
</TD>
|
|
256
|
+
{showTags ? (
|
|
257
|
+
<TD>
|
|
258
|
+
{(entry.tags ?? []).length > 0 ? (
|
|
259
|
+
<TagChips tags={entry.tags ?? []} />
|
|
260
|
+
) : (
|
|
261
|
+
<span className="tai-muted">—</span>
|
|
262
|
+
)}
|
|
263
|
+
</TD>
|
|
264
|
+
) : null}
|
|
265
|
+
<TD>
|
|
266
|
+
<div className="tai-row">
|
|
267
|
+
<Button
|
|
268
|
+
aria-label={`View version ${String(entry.version)}`}
|
|
269
|
+
onClick={() => {
|
|
270
|
+
view(entry.version);
|
|
271
|
+
}}
|
|
272
|
+
>
|
|
273
|
+
View
|
|
274
|
+
</Button>
|
|
275
|
+
{canCompare ? (
|
|
276
|
+
<Button
|
|
277
|
+
// The name starts with the word the button is SHOWING,
|
|
278
|
+
// which is what WCAG 2.5.3 (Label in Name) asks: a constant
|
|
279
|
+
// "Compare …" would leave a button reading "Comparing…"
|
|
280
|
+
// named "Compare", and a voice-control user naming a control
|
|
281
|
+
// they cannot see. The button stays ENABLED while armed —
|
|
282
|
+
// unlike an Edit action, a second click on the armed row is
|
|
283
|
+
// the disarm, so there is nothing to take away.
|
|
284
|
+
aria-label={`${compareFrom === entry.version ? 'Comparing' : 'Compare'} version ${String(entry.version)}`}
|
|
285
|
+
onClick={() => {
|
|
286
|
+
compare(entry.version);
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
{compareFrom === entry.version ? 'Comparing…' : 'Compare'}
|
|
290
|
+
</Button>
|
|
291
|
+
) : null}
|
|
292
|
+
{onEditTags !== undefined && !readOnly ? (
|
|
293
|
+
<Button
|
|
294
|
+
aria-label={`Edit tags for version ${String(entry.version)}`}
|
|
295
|
+
onClick={() => {
|
|
296
|
+
setEditingTags(entry.version);
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
Edit tags
|
|
300
|
+
</Button>
|
|
301
|
+
) : null}
|
|
302
|
+
{readOnly ? null : (
|
|
303
|
+
<Button
|
|
304
|
+
variant="secondary"
|
|
305
|
+
aria-label={`Roll back to version ${String(entry.version)}`}
|
|
306
|
+
disabled={entry.is_current}
|
|
307
|
+
onClick={() => {
|
|
308
|
+
setConfirming(entry.version);
|
|
309
|
+
}}
|
|
310
|
+
>
|
|
311
|
+
Roll back
|
|
312
|
+
</Button>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
</TD>
|
|
316
|
+
</TR>
|
|
317
|
+
))}
|
|
318
|
+
</TBody>
|
|
319
|
+
</Table>
|
|
320
|
+
</ScrollRegion>
|
|
321
|
+
</Card>
|
|
322
|
+
|
|
323
|
+
{compareFrom !== null && comparePair === null ? (
|
|
324
|
+
<p role="status" className="tai-muted">
|
|
325
|
+
Comparing from version {compareFrom} — pick another version, or Compare again to cancel.
|
|
326
|
+
</p>
|
|
327
|
+
) : null}
|
|
328
|
+
|
|
329
|
+
{selectedEntry !== undefined ? (
|
|
330
|
+
<section className="tai-stack tai-stack-2">
|
|
331
|
+
<h3 className="tai-section-title">Version {selectedEntry.version} body</h3>
|
|
332
|
+
<Card>
|
|
333
|
+
<JsonTree
|
|
334
|
+
data={selectedEntry.body}
|
|
335
|
+
label={`Version ${String(selectedEntry.version)} body`}
|
|
336
|
+
/>
|
|
337
|
+
</Card>
|
|
338
|
+
</section>
|
|
339
|
+
) : null}
|
|
340
|
+
|
|
341
|
+
{comparePair !== null ? (
|
|
342
|
+
<Dialog
|
|
343
|
+
title={`v${String(comparePair.from)} compared with v${String(comparePair.to)}`}
|
|
344
|
+
description="A structural diff of the two version bodies. Arrays compare whole."
|
|
345
|
+
open
|
|
346
|
+
onOpenChange={(next) => {
|
|
347
|
+
if (!next) closeCompare();
|
|
348
|
+
}}
|
|
349
|
+
>
|
|
350
|
+
{/* A deep diff row can run wider than the dialog; it scrolls in place. */}
|
|
351
|
+
<ScrollRegion label="Version diff">
|
|
352
|
+
<JsonDiff before={fromBody} after={toBody} />
|
|
353
|
+
</ScrollRegion>
|
|
354
|
+
<div className="tai-dialog-actions">
|
|
355
|
+
<Button onClick={closeCompare}>Close</Button>
|
|
356
|
+
</div>
|
|
357
|
+
</Dialog>
|
|
358
|
+
) : null}
|
|
359
|
+
|
|
360
|
+
{editingEntry !== undefined && onEditTags !== undefined ? (
|
|
361
|
+
<EditTagsDialog
|
|
362
|
+
version={editingEntry.version}
|
|
363
|
+
initialTags={editingEntry.tags ?? []}
|
|
364
|
+
onSave={onEditTags}
|
|
365
|
+
onClose={() => {
|
|
366
|
+
setEditingTags(null);
|
|
367
|
+
}}
|
|
368
|
+
/>
|
|
369
|
+
) : null}
|
|
370
|
+
|
|
371
|
+
{confirming !== null ? (
|
|
372
|
+
<Dialog
|
|
373
|
+
title="Roll back version"
|
|
374
|
+
description={
|
|
375
|
+
`Make version ${String(confirming)} the active version? The version history is preserved.` +
|
|
376
|
+
(rollbackConfirmDescription !== undefined ? ` ${rollbackConfirmDescription}` : '')
|
|
377
|
+
}
|
|
378
|
+
open
|
|
379
|
+
onOpenChange={(next) => {
|
|
380
|
+
if (!next) setConfirming(null);
|
|
381
|
+
}}
|
|
382
|
+
>
|
|
383
|
+
{rollbackError !== undefined ? <ErrorState message={rollbackError} /> : null}
|
|
384
|
+
<div className="tai-dialog-actions">
|
|
385
|
+
<Button
|
|
386
|
+
onClick={() => {
|
|
387
|
+
setConfirming(null);
|
|
388
|
+
}}
|
|
389
|
+
>
|
|
390
|
+
Cancel
|
|
391
|
+
</Button>
|
|
392
|
+
<Button
|
|
393
|
+
variant="primary"
|
|
394
|
+
disabled={rollbackPending}
|
|
395
|
+
onClick={() => {
|
|
396
|
+
onRollback(confirming);
|
|
397
|
+
}}
|
|
398
|
+
>
|
|
399
|
+
{rollbackPending ? <Spinner label="Rolling back" /> : null}
|
|
400
|
+
Roll back
|
|
401
|
+
</Button>
|
|
402
|
+
</div>
|
|
403
|
+
</Dialog>
|
|
404
|
+
) : null}
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
}
|
package/src/css.d.ts
ADDED
package/src/download.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream a `Blob` (an export- or content-route response) to the user as a file
|
|
3
|
+
* download via a transient object URL. The object URL is revoked on the next tick —
|
|
4
|
+
* revoking it synchronously can cancel the download before the browser commits it.
|
|
5
|
+
*/
|
|
6
|
+
export function downloadBlob(blob: Blob, filename: string): void {
|
|
7
|
+
const url = URL.createObjectURL(blob);
|
|
8
|
+
const anchor = document.createElement('a');
|
|
9
|
+
anchor.href = url;
|
|
10
|
+
anchor.download = filename;
|
|
11
|
+
document.body.appendChild(anchor);
|
|
12
|
+
anchor.click();
|
|
13
|
+
anchor.remove();
|
|
14
|
+
setTimeout(() => {
|
|
15
|
+
URL.revokeObjectURL(url);
|
|
16
|
+
}, 0);
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime half of the `asChild` slot contract.
|
|
3
|
+
*
|
|
4
|
+
* Radix clones its own props onto the node an `asChild` slot is given, so the
|
|
5
|
+
* slot takes exactly one ELEMENT. `ReactElement` states half of that: it rejects
|
|
6
|
+
* a string, which would throw inside Radix anyway. It does NOT reject a
|
|
7
|
+
* FRAGMENT — `<></>` is a `ReactElement` whose `type` is the fragment symbol —
|
|
8
|
+
* and a fragment is the worse of the two, because React drops every prop cloned
|
|
9
|
+
* onto it: the opener renders, carries no `aria-haspopup`/`aria-expanded`/
|
|
10
|
+
* `aria-controls` and no handler, and fails nothing.
|
|
11
|
+
*/
|
|
12
|
+
import { Fragment } from 'react';
|
|
13
|
+
import type { ReactElement } from 'react';
|
|
14
|
+
|
|
15
|
+
/** Raises when an `asChild` slot is handed a fragment. `slot` names the prop. */
|
|
16
|
+
export function assertSlotElement(element: ReactElement, slot: string): void {
|
|
17
|
+
if (element.type !== Fragment) return;
|
|
18
|
+
throw new Error(
|
|
19
|
+
`${slot} takes a single element, not a fragment: React drops the props the ` +
|
|
20
|
+
'slot clones onto it, so the wiring and the handler would be lost.',
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ElicitationForm` — the Studio-as-elicit-client widget. It renders a
|
|
3
|
+
* FastMCP elicitation request (its `message` + `requestedSchema`) as a
|
|
4
|
+
* `SchemaForm`, validates the answer against the schema, and hands the typed
|
|
5
|
+
* answer back on submit. The schema is carried through intact, so the caller
|
|
6
|
+
* gets exactly the shape it asked for.
|
|
7
|
+
*
|
|
8
|
+
* Accept-or-submit only: this widget's job is to collect and return a valid
|
|
9
|
+
* answer. A decline/cancel is not modeled here (the platform bridge is
|
|
10
|
+
* accept-or-raise); the optional `onCancel` is a UI affordance, not an MCP
|
|
11
|
+
* decline round-trip. Reusable by any feature that must answer an elicit.
|
|
12
|
+
*/
|
|
13
|
+
import type { ReactNode } from 'react';
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
|
|
16
|
+
import { Button } from '../components/primitives';
|
|
17
|
+
import { defaultValueForSchema } from '../schema-form/default-value';
|
|
18
|
+
import { SchemaForm } from '../schema-form/SchemaForm';
|
|
19
|
+
import type { JsonSchema, SchemaFormErrors } from '../schema-form/types';
|
|
20
|
+
import { validateAgainstSchema } from '../schema-form/validate';
|
|
21
|
+
|
|
22
|
+
export interface ElicitationFormProps {
|
|
23
|
+
/** The elicit request's human-readable message. */
|
|
24
|
+
readonly message: string;
|
|
25
|
+
/** The elicit `requestedSchema` — the answer's form schema. */
|
|
26
|
+
readonly schema: JsonSchema;
|
|
27
|
+
/** Called with the validated answer when the human submits. */
|
|
28
|
+
readonly onSubmit: (answer: unknown) => void;
|
|
29
|
+
/** Optional cancel affordance (UI only; not an MCP decline). */
|
|
30
|
+
readonly onCancel?: () => void;
|
|
31
|
+
/** Disables submission while an answer is in flight. */
|
|
32
|
+
readonly busy?: boolean;
|
|
33
|
+
readonly submitLabel?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Render the elicit message + a schema-driven answer form. On submit the answer
|
|
38
|
+
* is validated against the schema; on success `onSubmit` receives the typed
|
|
39
|
+
* value, otherwise the per-field errors surface and nothing is emitted.
|
|
40
|
+
*/
|
|
41
|
+
export function ElicitationForm({
|
|
42
|
+
message,
|
|
43
|
+
schema,
|
|
44
|
+
onSubmit,
|
|
45
|
+
onCancel,
|
|
46
|
+
busy = false,
|
|
47
|
+
submitLabel = 'Submit',
|
|
48
|
+
}: ElicitationFormProps): ReactNode {
|
|
49
|
+
const [value, setValue] = useState<unknown>(() => defaultValueForSchema(schema));
|
|
50
|
+
const [errors, setErrors] = useState<SchemaFormErrors>({});
|
|
51
|
+
|
|
52
|
+
const submit = () => {
|
|
53
|
+
const found = validateAgainstSchema(schema, value);
|
|
54
|
+
setErrors(found);
|
|
55
|
+
if (Object.keys(found).length === 0) {
|
|
56
|
+
onSubmit(value);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="tai-stack" data-testid="elicitation-form">
|
|
62
|
+
{/* The message is text the Studio did not author — the prose measure keeps
|
|
63
|
+
a long elicit prompt to a readable line length. It applies to a
|
|
64
|
+
PARAGRAPH INSIDE `.tai-prose`, so the class goes on the wrapper. */}
|
|
65
|
+
<div className="tai-prose">
|
|
66
|
+
<p>{message}</p>
|
|
67
|
+
</div>
|
|
68
|
+
<SchemaForm
|
|
69
|
+
schema={schema}
|
|
70
|
+
value={value}
|
|
71
|
+
onChange={setValue}
|
|
72
|
+
errors={errors}
|
|
73
|
+
idPrefix="elicit-form"
|
|
74
|
+
/>
|
|
75
|
+
<div className="tai-row">
|
|
76
|
+
<Button variant="primary" onClick={submit} disabled={busy}>
|
|
77
|
+
{submitLabel}
|
|
78
|
+
</Button>
|
|
79
|
+
{onCancel && (
|
|
80
|
+
<Button variant="secondary" onClick={onCancel} disabled={busy}>
|
|
81
|
+
Cancel
|
|
82
|
+
</Button>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize an unknown thrown/rejected value (a TanStack Query `error`, a
|
|
3
|
+
* rejected mutation, a zod/`ApiError`) into a human-readable string for the loud
|
|
4
|
+
* `ErrorState` / `Field` surfaces. This only turns a caught value into
|
|
5
|
+
* displayable text — it never swallows the failure; the caller decides where to
|
|
6
|
+
* show it, and the message is always shown, never blank.
|
|
7
|
+
*/
|
|
8
|
+
export function errorMessage(error: unknown): string {
|
|
9
|
+
if (error instanceof Error) return error.message;
|
|
10
|
+
if (typeof error === 'string') return error;
|
|
11
|
+
return 'An unexpected error occurred.';
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for a preset/tool's list of extension combos, whose elements may carry
|
|
3
|
+
* author `config` (`{ name, config }`) alongside bare-name elements. The editors
|
|
4
|
+
* hold the full config-bearing elements as their value, so config rides through an
|
|
5
|
+
* edit directly; these helpers project a combo to its bare extension names where a
|
|
6
|
+
* name-only view is needed (a picker's selection, a branch-tool name, a badge row).
|
|
7
|
+
*/
|
|
8
|
+
import type { PresetExtensionElement } from '@tai42/api-client';
|
|
9
|
+
|
|
10
|
+
/** The bare extension name of one combo element (drops any `config`). */
|
|
11
|
+
export function extensionElementName(element: PresetExtensionElement): string {
|
|
12
|
+
return typeof element === 'string' ? element : element.name;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Project a combo's elements to their bare extension names (drops any `config`). */
|
|
16
|
+
export function comboElementNames(combo: readonly PresetExtensionElement[]): string[] {
|
|
17
|
+
return combo.map(extensionElementName);
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useApi` — the typed client, provided through an SDK-owned React context.
|
|
3
|
+
*
|
|
4
|
+
* SDK boundary invariant: this context lives INSIDE the shared module set
|
|
5
|
+
* (the SDK is a shared singleton across the plugin boundary), so a plugin calling
|
|
6
|
+
* `useApi()` reaches the SAME client the shell built. It must NEVER wrap a
|
|
7
|
+
* TanStack Query/Router context (those are deliberately NOT shared) — it stays a
|
|
8
|
+
* plain typed client. The `ApiClient` type is imported type-only, so there is no
|
|
9
|
+
* runtime dependency on @tai42/api-client (the SDK imports nothing internal at
|
|
10
|
+
* runtime).
|
|
11
|
+
*/
|
|
12
|
+
import { createContext, useContext } from 'react';
|
|
13
|
+
import type { ApiClient } from '@tai42/api-client';
|
|
14
|
+
|
|
15
|
+
const ApiContext = createContext<ApiClient | null>(null);
|
|
16
|
+
|
|
17
|
+
export const ApiProvider = ApiContext.Provider;
|
|
18
|
+
|
|
19
|
+
export function useApi(): ApiClient {
|
|
20
|
+
const client = useContext(ApiContext);
|
|
21
|
+
if (client === null) {
|
|
22
|
+
throw new Error('useApi must be used within an <ApiProvider> (the shell provides it)');
|
|
23
|
+
}
|
|
24
|
+
return client;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type { ApiClient };
|