@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,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ConfirmDialog` — a modal that asks the operator to confirm an action and then
|
|
3
|
+
* runs it. It owns the scaffolding every confirm shares: a Cancel button, the
|
|
4
|
+
* confirm button (showing a pending Spinner while the action runs and disabled
|
|
5
|
+
* meanwhile), and a loud `ErrorState` when the action fails. The caller supplies
|
|
6
|
+
* the prompt as `children` and drives the action through `onConfirm` /
|
|
7
|
+
* `isPending` / `error`.
|
|
8
|
+
*
|
|
9
|
+
* Mount it only while the confirmation is active; any close gesture — Cancel,
|
|
10
|
+
* Escape, the overlay — calls `onClose`. The confirm button is destructive
|
|
11
|
+
* (`danger`) by default; pass `confirmVariant` for a non-destructive confirm.
|
|
12
|
+
*
|
|
13
|
+
* The footer is the design system's `tai-dialog-actions` row, so every confirm
|
|
14
|
+
* across Studio lands its buttons in the same place.
|
|
15
|
+
*/
|
|
16
|
+
import type { ReactNode } from 'react';
|
|
17
|
+
|
|
18
|
+
import { Dialog } from './dialog';
|
|
19
|
+
import { Button, ErrorState, Spinner } from './primitives';
|
|
20
|
+
import { errorMessage } from '../errors';
|
|
21
|
+
|
|
22
|
+
export interface ConfirmDialogProps {
|
|
23
|
+
readonly title: string;
|
|
24
|
+
/** The confirm button's resting label (e.g. `Delete scope`). */
|
|
25
|
+
readonly confirmLabel: string;
|
|
26
|
+
/** The Spinner's accessible label while the action runs (e.g. `Deleting`). */
|
|
27
|
+
readonly pendingLabel: string;
|
|
28
|
+
readonly onConfirm: () => void;
|
|
29
|
+
readonly onClose: () => void;
|
|
30
|
+
readonly isPending: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The failed action's error, rendered loudly; omit/`null` while there is none.
|
|
33
|
+
*
|
|
34
|
+
* A thrown `Error` or the message on its own — `Field`'s `error` is a plain
|
|
35
|
+
* string, and the two props sit side by side on a form, so this one accepts
|
|
36
|
+
* that spelling too rather than making one surface's `error` unassignable to
|
|
37
|
+
* the other's.
|
|
38
|
+
*/
|
|
39
|
+
readonly error?: Error | string | null;
|
|
40
|
+
readonly confirmVariant?: 'primary' | 'danger';
|
|
41
|
+
/** The prompt body — what the operator is confirming. */
|
|
42
|
+
readonly children: ReactNode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ConfirmDialog({
|
|
46
|
+
title,
|
|
47
|
+
confirmLabel,
|
|
48
|
+
pendingLabel,
|
|
49
|
+
onConfirm,
|
|
50
|
+
onClose,
|
|
51
|
+
isPending,
|
|
52
|
+
error,
|
|
53
|
+
confirmVariant = 'danger',
|
|
54
|
+
children,
|
|
55
|
+
}: ConfirmDialogProps): ReactNode {
|
|
56
|
+
return (
|
|
57
|
+
<Dialog
|
|
58
|
+
title={title}
|
|
59
|
+
open
|
|
60
|
+
onOpenChange={(next) => {
|
|
61
|
+
if (!next) onClose();
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
{error != null ? <ErrorState message={errorMessage(error)} /> : null}
|
|
66
|
+
<div className="tai-dialog-actions">
|
|
67
|
+
<Button type="button" onClick={onClose}>
|
|
68
|
+
Cancel
|
|
69
|
+
</Button>
|
|
70
|
+
<Button type="button" variant={confirmVariant} disabled={isPending} onClick={onConfirm}>
|
|
71
|
+
{isPending ? <Spinner label={pendingLabel} /> : null}
|
|
72
|
+
{confirmLabel}
|
|
73
|
+
</Button>
|
|
74
|
+
</div>
|
|
75
|
+
</Dialog>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The design-system class each form control wears. The 36 px geometry, the
|
|
3
|
+
* boundary, the placeholder, and the hover/disabled/invalid states all live in
|
|
4
|
+
* `components.css` under these names — this module is the single place a
|
|
5
|
+
* control names the class, so no control re-derives that geometry inline.
|
|
6
|
+
*/
|
|
7
|
+
export const INPUT_CLASS = 'tai-input';
|
|
8
|
+
export const TEXTAREA_CLASS = 'tai-textarea';
|
|
9
|
+
export const SELECT_TRIGGER_CLASS = 'tai-select-trigger';
|
|
10
|
+
|
|
11
|
+
/** A control's class plus the caller's, which sorts last so it can override. */
|
|
12
|
+
export function controlClassName(base: string, className: string | undefined): string {
|
|
13
|
+
return className === undefined ? base : `${base} ${className}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `CopyField` — a show-once box for a freshly-minted secret (an API key or a
|
|
3
|
+
* minted backup key). It renders the value in a monospace, break-all,
|
|
4
|
+
* select-all box beside a copy button that writes the value to the clipboard and
|
|
5
|
+
* shows a transient "Copied" affordance. It is PRESENTATIONAL: the caller owns
|
|
6
|
+
* the value; this component fetches nothing and never re-reads the secret.
|
|
7
|
+
*
|
|
8
|
+
* The button holds BOTH states stacked in one grid cell, so the widest of them
|
|
9
|
+
* sets the width once and the flip never reflows the row. Only the active state
|
|
10
|
+
* is exposed to assistive tech, and that state's own words are the button's
|
|
11
|
+
* accessible name: a constant `aria-label` would leave a button reading "Copied"
|
|
12
|
+
* named "Copy", which fails WCAG 2.5.3 (Label in Name) and leaves a voice-control
|
|
13
|
+
* user naming a control they cannot see. The polite live region beside the button
|
|
14
|
+
* is what actually announces the flip.
|
|
15
|
+
*
|
|
16
|
+
* A clipboard write can FAIL — a permissions policy blocks it, or the page is
|
|
17
|
+
* not in a secure context, where `navigator.clipboard` does not exist at all.
|
|
18
|
+
* Either way the value is a secret shown once, so the failure is rendered as a
|
|
19
|
+
* visible `role="alert"` telling the reader to copy it by hand: throwing it at
|
|
20
|
+
* the console would leave the button looking inert and the secret unrecoverable.
|
|
21
|
+
*
|
|
22
|
+
* SAFETY: the value and caption render as TEXT (React escapes them) — never an
|
|
23
|
+
* HTML sink. The value is never logged, and neither is a failure — the alert
|
|
24
|
+
* names the reason, never the value. Pinned by a test.
|
|
25
|
+
*/
|
|
26
|
+
import { useEffect, useRef, useState } from 'react';
|
|
27
|
+
import type { CSSProperties } from 'react';
|
|
28
|
+
|
|
29
|
+
import { CheckIcon, CopyIcon, XCircleIcon } from './icons';
|
|
30
|
+
|
|
31
|
+
export interface CopyFieldProps {
|
|
32
|
+
readonly value: string;
|
|
33
|
+
readonly caption?: string;
|
|
34
|
+
readonly idPrefix?: string;
|
|
35
|
+
readonly label?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const COPIED_RESET_MS = 2000;
|
|
39
|
+
|
|
40
|
+
/** The button's two faces. Whichever is exposed is also its accessible name. */
|
|
41
|
+
const COPY_LABEL = 'Copy';
|
|
42
|
+
const COPIED_LABEL = 'Copied';
|
|
43
|
+
|
|
44
|
+
/** Announced once, and worded so it is never mistaken for the visible label. */
|
|
45
|
+
const COPIED_ANNOUNCEMENT = 'Copied to clipboard';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `lib.dom` declares `navigator.clipboard` as always present, but the platform
|
|
49
|
+
* exposes it only in a SECURE CONTEXT — over plain http it is not there at all.
|
|
50
|
+
* Reading the navigator through this shape admits the absence the DOM types deny,
|
|
51
|
+
* so the guard below is a real check rather than a cast around one.
|
|
52
|
+
*/
|
|
53
|
+
interface MaybeClipboard {
|
|
54
|
+
readonly clipboard?: Clipboard;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The clipboard this browser actually offers, or `undefined` when it offers none. */
|
|
58
|
+
function clipboardOf(host: MaybeClipboard): Clipboard | undefined {
|
|
59
|
+
return host.clipboard;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Shown when the browser offers no clipboard at all (any non-secure context). */
|
|
63
|
+
const NO_CLIPBOARD =
|
|
64
|
+
'This browser will not write to the clipboard here. Select the value above and copy it.';
|
|
65
|
+
|
|
66
|
+
/** Shown when the write is offered and refused. */
|
|
67
|
+
function writeFailed(reason: unknown): string {
|
|
68
|
+
const detail = reason instanceof Error ? reason.message : String(reason);
|
|
69
|
+
return `Copy failed: ${detail}. Select the value above and copy it.`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const valueStyle: CSSProperties = {
|
|
73
|
+
flex: 1,
|
|
74
|
+
padding: 'var(--tai-space-2) var(--tai-space-3)',
|
|
75
|
+
wordBreak: 'break-all',
|
|
76
|
+
userSelect: 'all',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Both states share one grid cell, so the button is as wide as the wider one. */
|
|
80
|
+
const statesStyle: CSSProperties = {
|
|
81
|
+
display: 'grid',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
justifyItems: 'center',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const stateStyle: CSSProperties = {
|
|
87
|
+
gridArea: '1 / 1',
|
|
88
|
+
display: 'inline-flex',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
gap: 'var(--tai-space-2)',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export function CopyField({ value, caption, idPrefix = 'copy-field', label }: CopyFieldProps) {
|
|
94
|
+
const [copied, setCopied] = useState(false);
|
|
95
|
+
const [copyError, setCopyError] = useState<string | undefined>(undefined);
|
|
96
|
+
const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
97
|
+
// The clipboard write is async, so a copy can still be in flight when the
|
|
98
|
+
// component goes away — the caller's dialog closes on the same click. Clearing
|
|
99
|
+
// the timer alone would not cover that: at unmount there is no timer yet, and
|
|
100
|
+
// the resolution that follows would start one nothing is left to clear. This
|
|
101
|
+
// flag is what the resolution checks before it touches state at all.
|
|
102
|
+
const mounted = useRef(true);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
mounted.current = true;
|
|
106
|
+
return () => {
|
|
107
|
+
mounted.current = false;
|
|
108
|
+
if (resetTimer.current !== null) clearTimeout(resetTimer.current);
|
|
109
|
+
};
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const handleCopy = (): void => {
|
|
113
|
+
const clipboard = clipboardOf(navigator);
|
|
114
|
+
if (clipboard === undefined) {
|
|
115
|
+
setCopyError(NO_CLIPBOARD);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
setCopyError(undefined);
|
|
119
|
+
void clipboard.writeText(value).then(
|
|
120
|
+
() => {
|
|
121
|
+
if (!mounted.current) return;
|
|
122
|
+
setCopied(true);
|
|
123
|
+
if (resetTimer.current !== null) clearTimeout(resetTimer.current);
|
|
124
|
+
resetTimer.current = setTimeout(() => {
|
|
125
|
+
setCopied(false);
|
|
126
|
+
}, COPIED_RESET_MS);
|
|
127
|
+
},
|
|
128
|
+
(error: unknown) => {
|
|
129
|
+
if (!mounted.current) return;
|
|
130
|
+
setCopyError(writeFailed(error));
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div data-testid={idPrefix} className="tai-stack tai-stack-2">
|
|
137
|
+
{label !== undefined ? <span className="tai-field-label">{label}</span> : null}
|
|
138
|
+
<div className="tai-row">
|
|
139
|
+
<code className="tai-code" style={valueStyle}>
|
|
140
|
+
{value}
|
|
141
|
+
</code>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
className="tai-btn tai-btn-secondary"
|
|
145
|
+
data-testid={`${idPrefix}-copy`}
|
|
146
|
+
onClick={handleCopy}
|
|
147
|
+
>
|
|
148
|
+
<span style={statesStyle}>
|
|
149
|
+
<span
|
|
150
|
+
aria-hidden={copied}
|
|
151
|
+
style={{ ...stateStyle, visibility: copied ? 'hidden' : 'visible' }}
|
|
152
|
+
>
|
|
153
|
+
<CopyIcon />
|
|
154
|
+
{COPY_LABEL}
|
|
155
|
+
</span>
|
|
156
|
+
<span
|
|
157
|
+
aria-hidden={!copied}
|
|
158
|
+
style={{ ...stateStyle, visibility: copied ? 'visible' : 'hidden' }}
|
|
159
|
+
>
|
|
160
|
+
<CheckIcon />
|
|
161
|
+
{COPIED_LABEL}
|
|
162
|
+
</span>
|
|
163
|
+
</span>
|
|
164
|
+
</button>
|
|
165
|
+
<span aria-live="polite" className="tai-visually-hidden">
|
|
166
|
+
{copied ? COPIED_ANNOUNCEMENT : ''}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
{copyError !== undefined ? (
|
|
170
|
+
<span role="alert" className="tai-field-error">
|
|
171
|
+
<XCircleIcon />
|
|
172
|
+
{copyError}
|
|
173
|
+
</span>
|
|
174
|
+
) : null}
|
|
175
|
+
{caption !== undefined ? <span className="tai-field-hint">{caption}</span> : null}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Dialog` — a design-system wrapper over Radix Dialog: an accessible modal (role
|
|
3
|
+
* `dialog`, focus trap, Escape to close) labelled by its title. Modality is
|
|
4
|
+
* enforced by the focus trap and by Radix marking the rest of the page inert, not
|
|
5
|
+
* by an `aria-modal` attribute — the panel ships none.
|
|
6
|
+
*
|
|
7
|
+
* Radix owns every modal behaviour — the focus trap, the background scroll lock,
|
|
8
|
+
* Escape, and focus return to the trigger — so this component adds no second
|
|
9
|
+
* mechanism. It contributes only the design-system surface: `tai-overlay` paints
|
|
10
|
+
* the scrim from `--tai-color-scrim`, and `tai-dialog` sizes the panel with a
|
|
11
|
+
* `min()` so it still fits a 320 px viewport. The one gap Radix leaves — focus
|
|
12
|
+
* return for a dialog that renders no trigger — is filled by
|
|
13
|
+
* `useModalFocusReturn`, which stands down whenever a trigger IS rendered.
|
|
14
|
+
*/
|
|
15
|
+
import * as RadixDialog from '@radix-ui/react-dialog';
|
|
16
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
17
|
+
|
|
18
|
+
import { assertSlotElement } from '../element-slot';
|
|
19
|
+
import { useModalFocusReturn } from './modal-focus';
|
|
20
|
+
|
|
21
|
+
export interface DialogProps {
|
|
22
|
+
readonly title: string;
|
|
23
|
+
readonly description?: string;
|
|
24
|
+
readonly children?: ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* A trigger element; when omitted, control the dialog via `open`/`onOpenChange`.
|
|
27
|
+
*
|
|
28
|
+
* It is a single ELEMENT, not any node: Radix clones its props onto it, so a
|
|
29
|
+
* string throws. The type admits a FRAGMENT — `<></>` is a `ReactElement` —
|
|
30
|
+
* which would render openers carrying neither the
|
|
31
|
+
* `aria-haspopup`/`aria-expanded`/`aria-controls` wiring nor a click handler,
|
|
32
|
+
* so {@link assertSlotElement} rejects that one at runtime.
|
|
33
|
+
*/
|
|
34
|
+
readonly trigger?: ReactElement;
|
|
35
|
+
readonly open?: boolean;
|
|
36
|
+
readonly defaultOpen?: boolean;
|
|
37
|
+
readonly onOpenChange?: (open: boolean) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function Dialog({
|
|
41
|
+
title,
|
|
42
|
+
description,
|
|
43
|
+
children,
|
|
44
|
+
trigger,
|
|
45
|
+
open,
|
|
46
|
+
defaultOpen,
|
|
47
|
+
onOpenChange,
|
|
48
|
+
}: DialogProps) {
|
|
49
|
+
const focusReturn = useModalFocusReturn(trigger !== undefined);
|
|
50
|
+
if (trigger !== undefined) assertSlotElement(trigger, 'Dialog `trigger`');
|
|
51
|
+
return (
|
|
52
|
+
<RadixDialog.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
|
|
53
|
+
{trigger !== undefined ? <RadixDialog.Trigger asChild>{trigger}</RadixDialog.Trigger> : null}
|
|
54
|
+
<RadixDialog.Portal>
|
|
55
|
+
<RadixDialog.Overlay className="tai-overlay" />
|
|
56
|
+
{/* Radix wires `aria-describedby` to its Description's id unconditionally,
|
|
57
|
+
so a dialog that renders no Description would ship a dangling IDREF.
|
|
58
|
+
Passing the prop as `undefined` is Radix's opt-out — it has to be
|
|
59
|
+
ABSENT when a description IS rendered, or it would clear the wiring. */}
|
|
60
|
+
<RadixDialog.Content
|
|
61
|
+
className="tai-dialog"
|
|
62
|
+
{...focusReturn}
|
|
63
|
+
{...(description === undefined ? { 'aria-describedby': undefined } : {})}
|
|
64
|
+
>
|
|
65
|
+
<RadixDialog.Title className="tai-dialog-title">{title}</RadixDialog.Title>
|
|
66
|
+
<div className="tai-stack">
|
|
67
|
+
{description !== undefined ? (
|
|
68
|
+
<RadixDialog.Description className="tai-muted">{description}</RadixDialog.Description>
|
|
69
|
+
) : null}
|
|
70
|
+
{children}
|
|
71
|
+
</div>
|
|
72
|
+
</RadixDialog.Content>
|
|
73
|
+
</RadixDialog.Portal>
|
|
74
|
+
</RadixDialog.Root>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Drawer` — a side-anchored, full-height modal panel, published for a screen
|
|
3
|
+
* that wants a modal it can anchor to an edge rather than centre. Nothing in
|
|
4
|
+
* Studio renders one today; it is SDK surface a plugin can reach for. It is the
|
|
5
|
+
* same Radix Dialog machinery `Dialog` uses, so
|
|
6
|
+
* the focus trap, the scroll lock, Escape and overlay dismissal, the inert
|
|
7
|
+
* background, and focus return to the opener all come from the primitive; only
|
|
8
|
+
* the anchoring differs, and that lives entirely in `.tai-drawer`.
|
|
9
|
+
*
|
|
10
|
+
* Pass the opener as `trigger` where there is one (as `Dialog` does): Radix's own
|
|
11
|
+
* trigger is what carries `aria-haspopup`/`aria-expanded`/`aria-controls` and
|
|
12
|
+
* what its close sequence hands focus back to. A drawer opened purely by flipping
|
|
13
|
+
* `open` renders no trigger, so `useModalFocusReturn` returns focus to the opener
|
|
14
|
+
* in that case alone — with a trigger present it stands down and Radix's own
|
|
15
|
+
* restore is the only mechanism.
|
|
16
|
+
*
|
|
17
|
+
* Open state takes either form, the same three props `Dialog` publishes:
|
|
18
|
+
* `open`/`onOpenChange` to drive it from the caller's state, `defaultOpen` (or
|
|
19
|
+
* nothing at all) to let Radix own it behind a `trigger`.
|
|
20
|
+
*/
|
|
21
|
+
import * as RadixDialog from '@radix-ui/react-dialog';
|
|
22
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
23
|
+
|
|
24
|
+
import { assertSlotElement } from '../element-slot';
|
|
25
|
+
import { CloseIcon } from './icons';
|
|
26
|
+
import { useModalFocusReturn } from './modal-focus';
|
|
27
|
+
|
|
28
|
+
export interface DrawerProps {
|
|
29
|
+
/** The dialog's accessible name. */
|
|
30
|
+
readonly title: string;
|
|
31
|
+
readonly children: ReactNode;
|
|
32
|
+
readonly side?: 'left' | 'right';
|
|
33
|
+
/**
|
|
34
|
+
* The opener. Radix labels it and returns focus to it when the drawer closes.
|
|
35
|
+
*
|
|
36
|
+
* It is a single ELEMENT, not any node: Radix clones its props onto it, so a
|
|
37
|
+
* string throws. The type admits a FRAGMENT — `<></>` is a `ReactElement` —
|
|
38
|
+
* which would render openers carrying neither the
|
|
39
|
+
* `aria-haspopup`/`aria-expanded`/`aria-controls` wiring nor a click handler,
|
|
40
|
+
* so {@link assertSlotElement} rejects that one at runtime.
|
|
41
|
+
*/
|
|
42
|
+
readonly trigger?: ReactElement;
|
|
43
|
+
readonly open?: boolean;
|
|
44
|
+
readonly defaultOpen?: boolean;
|
|
45
|
+
readonly onOpenChange?: (open: boolean) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function Drawer({
|
|
49
|
+
title,
|
|
50
|
+
children,
|
|
51
|
+
side = 'left',
|
|
52
|
+
trigger,
|
|
53
|
+
open,
|
|
54
|
+
defaultOpen,
|
|
55
|
+
onOpenChange,
|
|
56
|
+
}: DrawerProps) {
|
|
57
|
+
const focusReturn = useModalFocusReturn(trigger !== undefined);
|
|
58
|
+
if (trigger !== undefined) assertSlotElement(trigger, 'Drawer `trigger`');
|
|
59
|
+
return (
|
|
60
|
+
<RadixDialog.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
|
|
61
|
+
{trigger === undefined ? null : <RadixDialog.Trigger asChild>{trigger}</RadixDialog.Trigger>}
|
|
62
|
+
<RadixDialog.Portal>
|
|
63
|
+
<RadixDialog.Overlay className="tai-overlay" />
|
|
64
|
+
{/* The panel is named by its title alone; `aria-describedby={undefined}`
|
|
65
|
+
is Radix's opt-out for a dialog that carries no description. */}
|
|
66
|
+
<RadixDialog.Content
|
|
67
|
+
className="tai-drawer"
|
|
68
|
+
data-side={side}
|
|
69
|
+
aria-describedby={undefined}
|
|
70
|
+
{...focusReturn}
|
|
71
|
+
>
|
|
72
|
+
<div className="tai-drawer-header">
|
|
73
|
+
<RadixDialog.Title className="tai-section-title">{title}</RadixDialog.Title>
|
|
74
|
+
<RadixDialog.Close className="tai-icon-btn" aria-label="Close">
|
|
75
|
+
<CloseIcon />
|
|
76
|
+
</RadixDialog.Close>
|
|
77
|
+
</div>
|
|
78
|
+
{children}
|
|
79
|
+
</RadixDialog.Content>
|
|
80
|
+
</RadixDialog.Portal>
|
|
81
|
+
</RadixDialog.Root>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A generic render-error boundary: it contains a throw from its subtree and shows
|
|
3
|
+
* a loud `role="alert"` panel in its place, so one failing region never blanks the
|
|
4
|
+
* page around it. `label` prefixes the caught message to name the contained region
|
|
5
|
+
* (e.g. the plugin whose content threw); `onError` observes the error for logging.
|
|
6
|
+
* There is no in-place retry — the boundary resets by remounting, so callers that
|
|
7
|
+
* want a reset give it a fresh `key`.
|
|
8
|
+
*
|
|
9
|
+
* The fallback is the design system's `ErrorState`, so a contained render error
|
|
10
|
+
* looks exactly like a failed fetch: one `role="alert"` surface headed by a mark
|
|
11
|
+
* and words, never color alone. It is rendered without a retry, since the
|
|
12
|
+
* boundary resets by remounting.
|
|
13
|
+
*/
|
|
14
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
15
|
+
|
|
16
|
+
import { errorMessage } from '../errors';
|
|
17
|
+
import { ErrorState } from './primitives';
|
|
18
|
+
|
|
19
|
+
export interface ErrorBoundaryProps {
|
|
20
|
+
readonly children: ReactNode;
|
|
21
|
+
/** Prefixed to the caught error message, naming the contained region. */
|
|
22
|
+
readonly label?: string;
|
|
23
|
+
/** Called with the caught error and React's component stack, for logging. */
|
|
24
|
+
readonly onError?: (error: unknown, info: ErrorInfo) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// React passes whatever the subtree threw — an `Error`, but also a bare string
|
|
28
|
+
// or any object a plugin chose to throw — so the caught value is `unknown` and
|
|
29
|
+
// coerced to a readable string only for display. `hasError` is the caught flag
|
|
30
|
+
// (not a sentinel on `error`), so even a falsy throw such as `null` is contained.
|
|
31
|
+
interface ErrorBoundaryState {
|
|
32
|
+
readonly hasError: boolean;
|
|
33
|
+
readonly error: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
37
|
+
override state: ErrorBoundaryState = { hasError: false, error: null };
|
|
38
|
+
|
|
39
|
+
static getDerivedStateFromError(error: unknown): ErrorBoundaryState {
|
|
40
|
+
return { hasError: true, error };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override componentDidCatch(error: unknown, info: ErrorInfo): void {
|
|
44
|
+
this.props.onError?.(error, info);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override render(): ReactNode {
|
|
48
|
+
if (this.state.hasError) {
|
|
49
|
+
const { label } = this.props;
|
|
50
|
+
const text = errorMessage(this.state.error);
|
|
51
|
+
const message = label !== undefined ? `${label}: ${text}` : text;
|
|
52
|
+
return <ErrorState message={message} />;
|
|
53
|
+
}
|
|
54
|
+
return this.props.children;
|
|
55
|
+
}
|
|
56
|
+
}
|