@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.
Files changed (422) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +15 -0
  3. package/README.md +318 -0
  4. package/SECURITY.md +51 -0
  5. package/dist/components/badge.d.ts +13 -0
  6. package/dist/components/badge.d.ts.map +1 -0
  7. package/dist/components/badge.js +14 -0
  8. package/dist/components/badge.js.map +1 -0
  9. package/dist/components/checkbox.d.ts +24 -0
  10. package/dist/components/checkbox.d.ts.map +1 -0
  11. package/dist/components/checkbox.js +31 -0
  12. package/dist/components/checkbox.js.map +1 -0
  13. package/dist/components/code-block.d.ts +7 -0
  14. package/dist/components/code-block.d.ts.map +1 -0
  15. package/dist/components/code-block.js +23 -0
  16. package/dist/components/code-block.js.map +1 -0
  17. package/dist/components/completion-input.d.ts +31 -0
  18. package/dist/components/completion-input.d.ts.map +1 -0
  19. package/dist/components/completion-input.js +61 -0
  20. package/dist/components/completion-input.js.map +1 -0
  21. package/dist/components/components.css +2212 -0
  22. package/dist/components/confirm-dialog.d.ts +40 -0
  23. package/dist/components/confirm-dialog.d.ts.map +1 -0
  24. package/dist/components/confirm-dialog.js +11 -0
  25. package/dist/components/confirm-dialog.js.map +1 -0
  26. package/dist/components/control-styles.d.ts +12 -0
  27. package/dist/components/control-styles.d.ts.map +1 -0
  28. package/dist/components/control-styles.js +14 -0
  29. package/dist/components/control-styles.js.map +1 -0
  30. package/dist/components/copy-field.d.ts +8 -0
  31. package/dist/components/copy-field.d.ts.map +1 -0
  32. package/dist/components/copy-field.js +106 -0
  33. package/dist/components/copy-field.js.map +1 -0
  34. package/dist/components/dialog.d.ts +21 -0
  35. package/dist/components/dialog.d.ts.map +1 -0
  36. package/dist/components/dialog.js +25 -0
  37. package/dist/components/dialog.js.map +1 -0
  38. package/dist/components/drawer.d.ts +22 -0
  39. package/dist/components/drawer.d.ts.map +1 -0
  40. package/dist/components/drawer.js +32 -0
  41. package/dist/components/drawer.js.map +1 -0
  42. package/dist/components/error-boundary.d.ts +33 -0
  43. package/dist/components/error-boundary.d.ts.map +1 -0
  44. package/dist/components/error-boundary.js +36 -0
  45. package/dist/components/error-boundary.js.map +1 -0
  46. package/dist/components/extension-combo-builder.d.ts +70 -0
  47. package/dist/components/extension-combo-builder.d.ts.map +1 -0
  48. package/dist/components/extension-combo-builder.js +154 -0
  49. package/dist/components/extension-combo-builder.js.map +1 -0
  50. package/dist/components/extension-grouping.d.ts +65 -0
  51. package/dist/components/extension-grouping.d.ts.map +1 -0
  52. package/dist/components/extension-grouping.js +78 -0
  53. package/dist/components/extension-grouping.js.map +1 -0
  54. package/dist/components/extension-picker.d.ts +35 -0
  55. package/dist/components/extension-picker.d.ts.map +1 -0
  56. package/dist/components/extension-picker.js +31 -0
  57. package/dist/components/extension-picker.js.map +1 -0
  58. package/dist/components/external-link.d.ts +19 -0
  59. package/dist/components/external-link.d.ts.map +1 -0
  60. package/dist/components/external-link.js +12 -0
  61. package/dist/components/external-link.js.map +1 -0
  62. package/dist/components/field.d.ts +64 -0
  63. package/dist/components/field.d.ts.map +1 -0
  64. package/dist/components/field.js +56 -0
  65. package/dist/components/field.js.map +1 -0
  66. package/dist/components/fleet-report.d.ts +39 -0
  67. package/dist/components/fleet-report.d.ts.map +1 -0
  68. package/dist/components/fleet-report.js +50 -0
  69. package/dist/components/fleet-report.js.map +1 -0
  70. package/dist/components/fonts.css +22 -0
  71. package/dist/components/icons.d.ts +142 -0
  72. package/dist/components/icons.d.ts.map +1 -0
  73. package/dist/components/icons.js +139 -0
  74. package/dist/components/icons.js.map +1 -0
  75. package/dist/components/inputs.d.ts +52 -0
  76. package/dist/components/inputs.d.ts.map +1 -0
  77. package/dist/components/inputs.js +35 -0
  78. package/dist/components/inputs.js.map +1 -0
  79. package/dist/components/json-diff.d.ts +40 -0
  80. package/dist/components/json-diff.d.ts.map +1 -0
  81. package/dist/components/json-diff.js +119 -0
  82. package/dist/components/json-diff.js.map +1 -0
  83. package/dist/components/json-tree.d.ts +8 -0
  84. package/dist/components/json-tree.d.ts.map +1 -0
  85. package/dist/components/json-tree.js +70 -0
  86. package/dist/components/json-tree.js.map +1 -0
  87. package/dist/components/modal-focus.d.ts +15 -0
  88. package/dist/components/modal-focus.d.ts.map +1 -0
  89. package/dist/components/modal-focus.js +136 -0
  90. package/dist/components/modal-focus.js.map +1 -0
  91. package/dist/components/page-header.d.ts +56 -0
  92. package/dist/components/page-header.d.ts.map +1 -0
  93. package/dist/components/page-header.js +24 -0
  94. package/dist/components/page-header.js.map +1 -0
  95. package/dist/components/primitives.d.ts +186 -0
  96. package/dist/components/primitives.d.ts.map +1 -0
  97. package/dist/components/primitives.js +204 -0
  98. package/dist/components/primitives.js.map +1 -0
  99. package/dist/components/progress-bar.d.ts +29 -0
  100. package/dist/components/progress-bar.d.ts.map +1 -0
  101. package/dist/components/progress-bar.js +48 -0
  102. package/dist/components/progress-bar.js.map +1 -0
  103. package/dist/components/radio-group.d.ts +37 -0
  104. package/dist/components/radio-group.d.ts.map +1 -0
  105. package/dist/components/radio-group.js +56 -0
  106. package/dist/components/radio-group.js.map +1 -0
  107. package/dist/components/reveal-input.d.ts +13 -0
  108. package/dist/components/reveal-input.d.ts.map +1 -0
  109. package/dist/components/reveal-input.js +31 -0
  110. package/dist/components/reveal-input.js.map +1 -0
  111. package/dist/components/scroll-region.d.ts +85 -0
  112. package/dist/components/scroll-region.d.ts.map +1 -0
  113. package/dist/components/scroll-region.js +574 -0
  114. package/dist/components/scroll-region.js.map +1 -0
  115. package/dist/components/select.d.ts +53 -0
  116. package/dist/components/select.d.ts.map +1 -0
  117. package/dist/components/select.js +33 -0
  118. package/dist/components/select.js.map +1 -0
  119. package/dist/components/table.d.ts +59 -0
  120. package/dist/components/table.d.ts.map +1 -0
  121. package/dist/components/table.js +20 -0
  122. package/dist/components/table.js.map +1 -0
  123. package/dist/components/tabs.d.ts +15 -0
  124. package/dist/components/tabs.d.ts.map +1 -0
  125. package/dist/components/tabs.js +13 -0
  126. package/dist/components/tabs.js.map +1 -0
  127. package/dist/components/tags.d.ts +46 -0
  128. package/dist/components/tags.d.ts.map +1 -0
  129. package/dist/components/tags.js +68 -0
  130. package/dist/components/tags.js.map +1 -0
  131. package/dist/components/tokens.css +392 -0
  132. package/dist/components/tokens.d.ts +15 -0
  133. package/dist/components/tokens.d.ts.map +1 -0
  134. package/dist/components/tokens.js +101 -0
  135. package/dist/components/tokens.js.map +1 -0
  136. package/dist/components/tool-picker.d.ts +24 -0
  137. package/dist/components/tool-picker.d.ts.map +1 -0
  138. package/dist/components/tool-picker.js +97 -0
  139. package/dist/components/tool-picker.js.map +1 -0
  140. package/dist/components/tooltip.d.ts +17 -0
  141. package/dist/components/tooltip.d.ts.map +1 -0
  142. package/dist/components/tooltip.js +18 -0
  143. package/dist/components/tooltip.js.map +1 -0
  144. package/dist/components/version-history-panel.d.ts +71 -0
  145. package/dist/components/version-history-panel.d.ts.map +1 -0
  146. package/dist/components/version-history-panel.js +148 -0
  147. package/dist/components/version-history-panel.js.map +1 -0
  148. package/dist/download.d.ts +7 -0
  149. package/dist/download.d.ts.map +1 -0
  150. package/dist/download.js +18 -0
  151. package/dist/download.js.map +1 -0
  152. package/dist/element-slot.d.ts +4 -0
  153. package/dist/element-slot.d.ts.map +1 -0
  154. package/dist/element-slot.js +20 -0
  155. package/dist/element-slot.js.map +1 -0
  156. package/dist/elicitation/ElicitationForm.d.ts +34 -0
  157. package/dist/elicitation/ElicitationForm.d.ts.map +1 -0
  158. package/dist/elicitation/ElicitationForm.js +24 -0
  159. package/dist/elicitation/ElicitationForm.js.map +1 -0
  160. package/dist/errors.d.ts +9 -0
  161. package/dist/errors.d.ts.map +1 -0
  162. package/dist/errors.js +15 -0
  163. package/dist/errors.js.map +1 -0
  164. package/dist/extension-combos.d.ts +13 -0
  165. package/dist/extension-combos.d.ts.map +1 -0
  166. package/dist/extension-combos.js +9 -0
  167. package/dist/extension-combos.js.map +1 -0
  168. package/dist/hooks/useApi.d.ts +1222 -0
  169. package/dist/hooks/useApi.d.ts.map +1 -0
  170. package/dist/hooks/useApi.js +22 -0
  171. package/dist/hooks/useApi.js.map +1 -0
  172. package/dist/hooks/useAuth.d.ts +13 -0
  173. package/dist/hooks/useAuth.d.ts.map +1 -0
  174. package/dist/hooks/useAuth.js +53 -0
  175. package/dist/hooks/useAuth.js.map +1 -0
  176. package/dist/hooks/useBreakpoint.d.ts +10 -0
  177. package/dist/hooks/useBreakpoint.d.ts.map +1 -0
  178. package/dist/hooks/useBreakpoint.js +66 -0
  179. package/dist/hooks/useBreakpoint.js.map +1 -0
  180. package/dist/hooks/useCapabilities.d.ts +88 -0
  181. package/dist/hooks/useCapabilities.d.ts.map +1 -0
  182. package/dist/hooks/useCapabilities.js +203 -0
  183. package/dist/hooks/useCapabilities.js.map +1 -0
  184. package/dist/hooks/useSse.d.ts +13 -0
  185. package/dist/hooks/useSse.d.ts.map +1 -0
  186. package/dist/hooks/useSse.js +298 -0
  187. package/dist/hooks/useSse.js.map +1 -0
  188. package/dist/hooks/useTheme.d.ts +20 -0
  189. package/dist/hooks/useTheme.d.ts.map +1 -0
  190. package/dist/hooks/useTheme.js +97 -0
  191. package/dist/hooks/useTheme.js.map +1 -0
  192. package/dist/hooks/useUnauthorized.d.ts +3 -0
  193. package/dist/hooks/useUnauthorized.d.ts.map +1 -0
  194. package/dist/hooks/useUnauthorized.js +18 -0
  195. package/dist/hooks/useUnauthorized.js.map +1 -0
  196. package/dist/host.d.ts +11 -0
  197. package/dist/host.d.ts.map +1 -0
  198. package/dist/host.js +10 -0
  199. package/dist/host.js.map +1 -0
  200. package/dist/index.d.ts +118 -0
  201. package/dist/index.d.ts.map +1 -0
  202. package/dist/index.js +91 -0
  203. package/dist/index.js.map +1 -0
  204. package/dist/navigation/context.d.ts +25 -0
  205. package/dist/navigation/context.d.ts.map +1 -0
  206. package/dist/navigation/context.js +45 -0
  207. package/dist/navigation/context.js.map +1 -0
  208. package/dist/navigation/index.d.ts +4 -0
  209. package/dist/navigation/index.d.ts.map +1 -0
  210. package/dist/navigation/index.js +2 -0
  211. package/dist/navigation/index.js.map +1 -0
  212. package/dist/navigation/types.d.ts +97 -0
  213. package/dist/navigation/types.d.ts.map +1 -0
  214. package/dist/navigation/types.js +16 -0
  215. package/dist/navigation/types.js.map +1 -0
  216. package/dist/plugin/host-state.d.ts +45 -0
  217. package/dist/plugin/host-state.d.ts.map +1 -0
  218. package/dist/plugin/host-state.js +62 -0
  219. package/dist/plugin/host-state.js.map +1 -0
  220. package/dist/plugin/registry.d.ts +35 -0
  221. package/dist/plugin/registry.d.ts.map +1 -0
  222. package/dist/plugin/registry.js +129 -0
  223. package/dist/plugin/registry.js.map +1 -0
  224. package/dist/plugin/types.d.ts +165 -0
  225. package/dist/plugin/types.d.ts.map +1 -0
  226. package/dist/plugin/types.js +2 -0
  227. package/dist/plugin/types.js.map +1 -0
  228. package/dist/plugin/version.d.ts +34 -0
  229. package/dist/plugin/version.d.ts.map +1 -0
  230. package/dist/plugin/version.js +37 -0
  231. package/dist/plugin/version.js.map +1 -0
  232. package/dist/query-keys.d.ts +31 -0
  233. package/dist/query-keys.d.ts.map +1 -0
  234. package/dist/query-keys.js +31 -0
  235. package/dist/query-keys.js.map +1 -0
  236. package/dist/schema-editor/SchemaEditor.d.ts +43 -0
  237. package/dist/schema-editor/SchemaEditor.d.ts.map +1 -0
  238. package/dist/schema-editor/SchemaEditor.js +106 -0
  239. package/dist/schema-editor/SchemaEditor.js.map +1 -0
  240. package/dist/schema-editor/index.d.ts +10 -0
  241. package/dist/schema-editor/index.d.ts.map +1 -0
  242. package/dist/schema-editor/index.js +8 -0
  243. package/dist/schema-editor/index.js.map +1 -0
  244. package/dist/schema-editor/lint.d.ts +37 -0
  245. package/dist/schema-editor/lint.d.ts.map +1 -0
  246. package/dist/schema-editor/lint.js +49 -0
  247. package/dist/schema-editor/lint.js.map +1 -0
  248. package/dist/schema-form/SchemaForm.d.ts +70 -0
  249. package/dist/schema-form/SchemaForm.d.ts.map +1 -0
  250. package/dist/schema-form/SchemaForm.js +13 -0
  251. package/dist/schema-form/SchemaForm.js.map +1 -0
  252. package/dist/schema-form/array-field.d.ts +20 -0
  253. package/dist/schema-form/array-field.d.ts.map +1 -0
  254. package/dist/schema-form/array-field.js +39 -0
  255. package/dist/schema-form/array-field.js.map +1 -0
  256. package/dist/schema-form/classify.d.ts +87 -0
  257. package/dist/schema-form/classify.d.ts.map +1 -0
  258. package/dist/schema-form/classify.js +199 -0
  259. package/dist/schema-form/classify.js.map +1 -0
  260. package/dist/schema-form/context.d.ts +4 -0
  261. package/dist/schema-form/context.d.ts.map +1 -0
  262. package/dist/schema-form/context.js +14 -0
  263. package/dist/schema-form/context.js.map +1 -0
  264. package/dist/schema-form/default-value.d.ts +8 -0
  265. package/dist/schema-form/default-value.d.ts.map +1 -0
  266. package/dist/schema-form/default-value.js +64 -0
  267. package/dist/schema-form/default-value.js.map +1 -0
  268. package/dist/schema-form/enum-field.d.ts +19 -0
  269. package/dist/schema-form/enum-field.d.ts.map +1 -0
  270. package/dist/schema-form/enum-field.js +26 -0
  271. package/dist/schema-form/enum-field.js.map +1 -0
  272. package/dist/schema-form/field-group.d.ts +9 -0
  273. package/dist/schema-form/field-group.d.ts.map +1 -0
  274. package/dist/schema-form/field-group.js +32 -0
  275. package/dist/schema-form/field-group.js.map +1 -0
  276. package/dist/schema-form/field-node.d.ts +24 -0
  277. package/dist/schema-form/field-node.d.ts.map +1 -0
  278. package/dist/schema-form/field-node.js +51 -0
  279. package/dist/schema-form/field-node.js.map +1 -0
  280. package/dist/schema-form/index.d.ts +13 -0
  281. package/dist/schema-form/index.d.ts.map +1 -0
  282. package/dist/schema-form/index.js +11 -0
  283. package/dist/schema-form/index.js.map +1 -0
  284. package/dist/schema-form/media-field.d.ts +24 -0
  285. package/dist/schema-form/media-field.d.ts.map +1 -0
  286. package/dist/schema-form/media-field.js +169 -0
  287. package/dist/schema-form/media-field.js.map +1 -0
  288. package/dist/schema-form/media.d.ts +40 -0
  289. package/dist/schema-form/media.d.ts.map +1 -0
  290. package/dist/schema-form/media.js +65 -0
  291. package/dist/schema-form/media.js.map +1 -0
  292. package/dist/schema-form/object-fields.d.ts +20 -0
  293. package/dist/schema-form/object-fields.d.ts.map +1 -0
  294. package/dist/schema-form/object-fields.js +24 -0
  295. package/dist/schema-form/object-fields.js.map +1 -0
  296. package/dist/schema-form/resolve.d.ts +41 -0
  297. package/dist/schema-form/resolve.d.ts.map +1 -0
  298. package/dist/schema-form/resolve.js +102 -0
  299. package/dist/schema-form/resolve.js.map +1 -0
  300. package/dist/schema-form/string-field.d.ts +20 -0
  301. package/dist/schema-form/string-field.d.ts.map +1 -0
  302. package/dist/schema-form/string-field.js +68 -0
  303. package/dist/schema-form/string-field.js.map +1 -0
  304. package/dist/schema-form/styles.d.ts +18 -0
  305. package/dist/schema-form/styles.d.ts.map +1 -0
  306. package/dist/schema-form/styles.js +18 -0
  307. package/dist/schema-form/styles.js.map +1 -0
  308. package/dist/schema-form/types.d.ts +67 -0
  309. package/dist/schema-form/types.d.ts.map +1 -0
  310. package/dist/schema-form/types.js +10 -0
  311. package/dist/schema-form/types.js.map +1 -0
  312. package/dist/schema-form/union-field.d.ts +24 -0
  313. package/dist/schema-form/union-field.d.ts.map +1 -0
  314. package/dist/schema-form/union-field.js +55 -0
  315. package/dist/schema-form/union-field.js.map +1 -0
  316. package/dist/schema-form/union.d.ts +4 -0
  317. package/dist/schema-form/union.d.ts.map +1 -0
  318. package/dist/schema-form/union.js +68 -0
  319. package/dist/schema-form/union.js.map +1 -0
  320. package/dist/schema-form/unsupported-notice.d.ts +18 -0
  321. package/dist/schema-form/unsupported-notice.d.ts.map +1 -0
  322. package/dist/schema-form/unsupported-notice.js +8 -0
  323. package/dist/schema-form/unsupported-notice.js.map +1 -0
  324. package/dist/schema-form/validate.d.ts +18 -0
  325. package/dist/schema-form/validate.d.ts.map +1 -0
  326. package/dist/schema-form/validate.js +162 -0
  327. package/dist/schema-form/validate.js.map +1 -0
  328. package/dist/structured-output/StructuredOutput.d.ts +27 -0
  329. package/dist/structured-output/StructuredOutput.d.ts.map +1 -0
  330. package/dist/structured-output/StructuredOutput.js +41 -0
  331. package/dist/structured-output/StructuredOutput.js.map +1 -0
  332. package/dist/testing.d.ts +80 -0
  333. package/dist/testing.d.ts.map +1 -0
  334. package/dist/testing.js +207 -0
  335. package/dist/testing.js.map +1 -0
  336. package/package.json +101 -0
  337. package/src/components/badge.tsx +31 -0
  338. package/src/components/checkbox.tsx +93 -0
  339. package/src/components/code-block.tsx +37 -0
  340. package/src/components/completion-input.tsx +137 -0
  341. package/src/components/components.css +2212 -0
  342. package/src/components/confirm-dialog.tsx +77 -0
  343. package/src/components/control-styles.ts +14 -0
  344. package/src/components/copy-field.tsx +178 -0
  345. package/src/components/dialog.tsx +76 -0
  346. package/src/components/drawer.tsx +83 -0
  347. package/src/components/error-boundary.tsx +56 -0
  348. package/src/components/extension-combo-builder.tsx +305 -0
  349. package/src/components/extension-grouping.ts +116 -0
  350. package/src/components/extension-picker.tsx +97 -0
  351. package/src/components/external-link.tsx +40 -0
  352. package/src/components/field.tsx +169 -0
  353. package/src/components/fleet-report.tsx +136 -0
  354. package/src/components/fonts.css +22 -0
  355. package/src/components/icons.tsx +504 -0
  356. package/src/components/inputs.tsx +125 -0
  357. package/src/components/json-diff.tsx +192 -0
  358. package/src/components/json-tree.tsx +129 -0
  359. package/src/components/modal-focus.ts +150 -0
  360. package/src/components/page-header.tsx +107 -0
  361. package/src/components/primitives.tsx +489 -0
  362. package/src/components/progress-bar.tsx +100 -0
  363. package/src/components/radio-group.tsx +160 -0
  364. package/src/components/reveal-input.tsx +87 -0
  365. package/src/components/scroll-region.tsx +703 -0
  366. package/src/components/select.tsx +149 -0
  367. package/src/components/table.tsx +94 -0
  368. package/src/components/tabs.tsx +51 -0
  369. package/src/components/tags.tsx +137 -0
  370. package/src/components/tokens.css +392 -0
  371. package/src/components/tokens.ts +102 -0
  372. package/src/components/tool-picker.tsx +175 -0
  373. package/src/components/tooltip.tsx +46 -0
  374. package/src/components/version-history-panel.tsx +407 -0
  375. package/src/css.d.ts +2 -0
  376. package/src/download.ts +17 -0
  377. package/src/element-slot.ts +22 -0
  378. package/src/elicitation/ElicitationForm.tsx +87 -0
  379. package/src/errors.ts +12 -0
  380. package/src/extension-combos.ts +18 -0
  381. package/src/hooks/useApi.ts +27 -0
  382. package/src/hooks/useAuth.ts +67 -0
  383. package/src/hooks/useBreakpoint.ts +78 -0
  384. package/src/hooks/useCapabilities.ts +242 -0
  385. package/src/hooks/useSse.ts +314 -0
  386. package/src/hooks/useTheme.ts +133 -0
  387. package/src/hooks/useUnauthorized.ts +20 -0
  388. package/src/host.ts +15 -0
  389. package/src/index.ts +267 -0
  390. package/src/navigation/context.tsx +90 -0
  391. package/src/navigation/index.ts +9 -0
  392. package/src/navigation/types.ts +90 -0
  393. package/src/plugin/host-state.ts +93 -0
  394. package/src/plugin/registry.ts +174 -0
  395. package/src/plugin/types.ts +178 -0
  396. package/src/plugin/version.ts +44 -0
  397. package/src/query-keys.ts +34 -0
  398. package/src/schema-editor/SchemaEditor.tsx +186 -0
  399. package/src/schema-editor/index.ts +9 -0
  400. package/src/schema-editor/lint.ts +70 -0
  401. package/src/schema-form/SchemaForm.tsx +107 -0
  402. package/src/schema-form/array-field.tsx +110 -0
  403. package/src/schema-form/classify.ts +302 -0
  404. package/src/schema-form/context.ts +17 -0
  405. package/src/schema-form/default-value.ts +65 -0
  406. package/src/schema-form/enum-field.tsx +65 -0
  407. package/src/schema-form/field-group.tsx +71 -0
  408. package/src/schema-form/field-node.tsx +161 -0
  409. package/src/schema-form/index.ts +12 -0
  410. package/src/schema-form/media-field.tsx +281 -0
  411. package/src/schema-form/media.ts +69 -0
  412. package/src/schema-form/object-fields.tsx +74 -0
  413. package/src/schema-form/resolve.ts +114 -0
  414. package/src/schema-form/string-field.tsx +161 -0
  415. package/src/schema-form/styles.ts +20 -0
  416. package/src/schema-form/types.ts +71 -0
  417. package/src/schema-form/union-field.tsx +122 -0
  418. package/src/schema-form/union.ts +78 -0
  419. package/src/schema-form/unsupported-notice.tsx +39 -0
  420. package/src/schema-form/validate.ts +183 -0
  421. package/src/structured-output/StructuredOutput.tsx +81 -0
  422. package/src/testing.ts +235 -0
@@ -0,0 +1,305 @@
1
+ /**
2
+ * `ExtensionComboBuilder` — a controlled editor for a preset/tool's LIST OF
3
+ * extension combos. A combo is an ordered list of extension ELEMENTS, each a bare
4
+ * extension NAME or a `{ name, config }` mapping carrying author config; the value
5
+ * is the list of combos (`PresetExtensionElement[][]`), the SAME shape the
6
+ * preset/tool extensions wire carries. It is PRESENTATIONAL: the caller fetches the
7
+ * extension catalog (`available`) and owns the `value`; this component fetches nothing.
8
+ *
9
+ * The surface has two parts:
10
+ * - the current combos, each an ordered row of name `Badge`s with a per-combo Edit
11
+ * (pull it back into the draft) and Remove;
12
+ * - a combo UNDER CONSTRUCTION, authored with the shared `ExtensionPicker` (grouped
13
+ * by kind, single-select for the non-stackable `backend` kind). When the draft
14
+ * includes the `output_schema` extension, its config is authored inline with a
15
+ * `SchemaEditor` (no title required); an "Add combo" / "Update combo" action
16
+ * composes the elements — `output_schema` as `{ name, config: { schema } }`, every
17
+ * other extension as a bare name — and writes it into the value.
18
+ *
19
+ * The builder only blocks the client-trivial invalids: an EMPTY combo (Add is
20
+ * disabled until the draft has ≥1 member), a DUPLICATE of a combo already in the
21
+ * value (by name sequence; flagged inline, Add blocked), an INVALID `output_schema`
22
+ * config (the `SchemaEditor` shows the parse/lint error and Add is blocked), and an
23
+ * UNKNOWN extension name in an existing combo (a name absent from `available`, which
24
+ * happens when a plugin is removed after a preset was authored) — flagged inline on
25
+ * that combo row and reported through `onValidityChange` so the caller can block
26
+ * submit. Every deeper rule — the registry's one-per-non-stackable-kind constraint,
27
+ * meta-schema validity, kind/ordering — is SERVER-AUTHORITATIVE: the write route
28
+ * validates each combo and its 400 message is surfaced verbatim by the caller.
29
+ *
30
+ * Loading is signaled by `availableReady`: while the catalog is still loading the
31
+ * builder reports VALID and suppresses the unknown-name notes (no danger-note flash
32
+ * while submit stays enabled — the server backstops), so a stale-seeded combo does
33
+ * not flicker invalid before the catalog resolves.
34
+ *
35
+ * SAFETY: an extension name is server-supplied, so every name renders as TEXT through
36
+ * the DS `Badge`/`Checkbox` (React escapes it) — never an HTML sink.
37
+ *
38
+ * Geometry and ink come from the design-system classes: each committed combo is a
39
+ * `tai-card` row, the invalid notes are `tai-field-error` (icon + message, never
40
+ * color alone), and Remove is a labelled `Button` carrying `CloseIcon` beside the
41
+ * word, under an accessible name that names the combo it removes.
42
+ */
43
+ import { Fragment, useEffect, useState, type ReactNode } from 'react';
44
+
45
+ import type { Extension, PresetExtensionElement } from '@tai42/api-client';
46
+
47
+ import { comboElementNames, extensionElementName } from '../extension-combos';
48
+ import { SchemaEditor, type SchemaEditorChange } from '../schema-editor';
49
+ import { Badge } from './badge';
50
+ import { ExtensionPicker } from './extension-picker';
51
+ import { CloseIcon, XCircleIcon } from './icons';
52
+ import { Button } from './primitives';
53
+
54
+ /** The one config-taking extension this builder authors inline. */
55
+ const OUTPUT_SCHEMA = 'output_schema';
56
+
57
+ export interface ExtensionComboBuilderProps {
58
+ /** The extension catalog the combos are drawn from (caller-fetched). */
59
+ readonly available: readonly Extension[];
60
+ /** The current list of combos, each an ordered list of extension elements. */
61
+ readonly value: readonly PresetExtensionElement[][];
62
+ /** Fired with the next list of combos on every add/update/remove. */
63
+ readonly onChange: (next: PresetExtensionElement[][]) => void;
64
+ readonly disabled?: boolean;
65
+ readonly idPrefix?: string;
66
+ /**
67
+ * Fired (effect-driven) on mount and whenever `value`, `available`, or
68
+ * `availableReady` changes with whether every combo's names are known. A caller
69
+ * blocks submit while this is `false`. The `onChange` payload shape is unchanged;
70
+ * this is a SECOND callback so the prop surface stays backward-compatible.
71
+ */
72
+ readonly onValidityChange?: (valid: boolean) => void;
73
+ /**
74
+ * Whether the `available` catalog has finished loading. Default `true` (existing
75
+ * consumers keep today's behavior). While `false` the builder reports VALID and
76
+ * suppresses the unknown-name notes, so a stale-seeded combo does not flash invalid
77
+ * before the catalog resolves.
78
+ */
79
+ readonly availableReady?: boolean;
80
+ }
81
+
82
+ /** Two combos are equal when their NAME sequences match (config is not part of identity). */
83
+ function sameNames(a: readonly string[], b: readonly string[]): boolean {
84
+ return a.length === b.length && a.every((name, index) => name === b[index]);
85
+ }
86
+
87
+ /** The stored `output_schema` config schema for a combo, if it carries one. */
88
+ function outputSchemaConfig(
89
+ combo: readonly PresetExtensionElement[],
90
+ ): Record<string, unknown> | null {
91
+ for (const element of combo) {
92
+ if (typeof element !== 'string' && element.name === OUTPUT_SCHEMA) {
93
+ const schema = element.config.schema;
94
+ return typeof schema === 'object' && schema !== null && !Array.isArray(schema)
95
+ ? (schema as Record<string, unknown>)
96
+ : {};
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /** The element names in a combo that are absent from the known catalog. */
103
+ function unknownNames(
104
+ combo: readonly PresetExtensionElement[],
105
+ availableNames: ReadonlySet<string>,
106
+ ): string[] {
107
+ return comboElementNames(combo).filter((name) => !availableNames.has(name));
108
+ }
109
+
110
+ export function ExtensionComboBuilder({
111
+ available,
112
+ value,
113
+ onChange,
114
+ disabled,
115
+ idPrefix = 'extension-combo-builder',
116
+ onValidityChange,
117
+ availableReady = true,
118
+ }: ExtensionComboBuilderProps): ReactNode {
119
+ const [draft, setDraft] = useState<string[]>([]);
120
+ const [draftConfig, setDraftConfig] = useState<SchemaEditorChange>({ schema: null, valid: true });
121
+ // The index of the combo being edited (pulled into the draft), or `null` for a new one.
122
+ const [editing, setEditing] = useState<number | null>(null);
123
+
124
+ const draftHasOutputSchema = draft.includes(OUTPUT_SCHEMA);
125
+ const isDuplicate =
126
+ draft.length > 0 &&
127
+ value.some((combo, index) => index !== editing && sameNames(comboElementNames(combo), draft));
128
+ const configInvalid = draftHasOutputSchema && !draftConfig.valid;
129
+ const canAdd = !disabled && draft.length > 0 && !isDuplicate && !configInvalid;
130
+
131
+ // Unknown-name validation over the EXISTING combos. Until the catalog is ready the
132
+ // builder reports VALID and suppresses the notes (no flash on a stale-seeded combo).
133
+ const availableNames = new Set(available.map((extension) => extension.name));
134
+ const hasUnknownName =
135
+ availableReady && value.some((combo) => unknownNames(combo, availableNames).length > 0);
136
+ const valid = !hasUnknownName;
137
+
138
+ useEffect(() => {
139
+ onValidityChange?.(valid);
140
+ }, [onValidityChange, valid]);
141
+
142
+ const resetDraft = (): void => {
143
+ setDraft([]);
144
+ setDraftConfig({ schema: null, valid: true });
145
+ setEditing(null);
146
+ };
147
+
148
+ /**
149
+ * Compose the draft names into config-bearing elements. `output_schema` takes the
150
+ * inline-authored config; every other name keeps the edited combo's existing
151
+ * element verbatim when it survives — preserving any author `config` (e.g. an
152
+ * `ask_external` verifier) the builder does not itself author — and a newly-added
153
+ * name becomes a bare-name element.
154
+ */
155
+ const composeCombo = (): PresetExtensionElement[] => {
156
+ const source = editing === null ? undefined : value[editing];
157
+ const byName = new Map<string, PresetExtensionElement>();
158
+ if (source !== undefined) {
159
+ for (const element of source) byName.set(extensionElementName(element), element);
160
+ }
161
+ return draft.map((name) =>
162
+ name === OUTPUT_SCHEMA
163
+ ? { name, config: { schema: draftConfig.schema ?? {} } }
164
+ : (byName.get(name) ?? name),
165
+ );
166
+ };
167
+
168
+ const commitCombo = (): void => {
169
+ if (!canAdd) return;
170
+ const composed = composeCombo();
171
+ if (editing === null) {
172
+ onChange([...value.map((combo) => [...combo]), composed]);
173
+ } else {
174
+ onChange(value.map((combo, index) => (index === editing ? composed : [...combo])));
175
+ }
176
+ resetDraft();
177
+ };
178
+
179
+ const startEdit = (index: number): void => {
180
+ const combo = value[index];
181
+ if (combo === undefined) return;
182
+ setDraft(comboElementNames(combo));
183
+ setDraftConfig({ schema: outputSchemaConfig(combo), valid: true });
184
+ setEditing(index);
185
+ };
186
+
187
+ const removeCombo = (index: number): void => {
188
+ onChange(value.filter((_, i) => i !== index).map((combo) => [...combo]));
189
+ // Removing the row under edit (or one before it) invalidates the draft target.
190
+ if (editing !== null && (editing === index || editing > index)) resetDraft();
191
+ };
192
+
193
+ return (
194
+ <div data-testid={idPrefix} className="tai-stack">
195
+ {value.length > 0 ? (
196
+ <ul className="tai-stack tai-stack-2">
197
+ {value.map((combo, index) => {
198
+ const names = comboElementNames(combo);
199
+ const unknown = availableReady ? unknownNames(combo, availableNames) : [];
200
+ return (
201
+ // The index IS the identity here: the list mutates by append, in-place
202
+ // update, and remove-by-index (no reorder), and a combo has no stable id
203
+ // of its own — so an index key is stable and cannot collide.
204
+ <li key={index} className="tai-card tai-stack tai-stack-2">
205
+ <div className="tai-row">
206
+ <span className="tai-row">
207
+ {names.map((name, position) => (
208
+ <Fragment key={`${name}-${String(position)}`}>
209
+ {position > 0 ? (
210
+ <span aria-hidden className="tai-muted">
211
+ +
212
+ </span>
213
+ ) : null}
214
+ <Badge variant={unknown.includes(name) ? 'danger' : 'primary'}>
215
+ {name}
216
+ </Badge>
217
+ </Fragment>
218
+ ))}
219
+ </span>
220
+ <Button
221
+ type="button"
222
+ // The name starts with the words the button is SHOWING, which
223
+ // is what WCAG 2.5.3 (Label in Name) asks: a constant "Edit …"
224
+ // would leave a button reading "Editing" named "Edit", and a
225
+ // voice-control user naming a control they cannot see.
226
+ aria-label={`${editing === index ? 'Editing' : 'Edit'} combo ${names.join('+')}`}
227
+ disabled={disabled === true || editing === index}
228
+ onClick={() => {
229
+ startEdit(index);
230
+ }}
231
+ >
232
+ {editing === index ? 'Editing' : 'Edit'}
233
+ </Button>
234
+ <Button
235
+ type="button"
236
+ aria-label={`Remove combo ${names.join('+')}`}
237
+ disabled={disabled}
238
+ onClick={() => {
239
+ removeCombo(index);
240
+ }}
241
+ >
242
+ <CloseIcon />
243
+ Remove
244
+ </Button>
245
+ </div>
246
+ {unknown.length > 0 ? (
247
+ <p role="alert" className="tai-field-error">
248
+ <XCircleIcon />
249
+ {unknown.length === 1 ? 'Unknown extension: ' : 'Unknown extensions: '}
250
+ {unknown.join(', ')}.
251
+ </p>
252
+ ) : null}
253
+ </li>
254
+ );
255
+ })}
256
+ </ul>
257
+ ) : (
258
+ <p className="tai-muted">No extension combos.</p>
259
+ )}
260
+
261
+ <div className="tai-stack tai-stack-2">
262
+ <ExtensionPicker
263
+ available={available}
264
+ value={draft}
265
+ onChange={setDraft}
266
+ disabled={disabled}
267
+ idPrefix={`${idPrefix}-draft`}
268
+ />
269
+
270
+ {draftHasOutputSchema ? (
271
+ <SchemaEditor
272
+ value={draftConfig.schema}
273
+ onChange={setDraftConfig}
274
+ requireTitle={false}
275
+ label="Output schema"
276
+ description="The JSON Schema the output_schema extension enforces on this combo's result."
277
+ disabled={disabled}
278
+ idPrefix={`${idPrefix}-output-schema`}
279
+ // Re-seed the editor when the draft target changes (a new draft vs a
280
+ // specific combo pulled in for edit).
281
+ key={editing === null ? 'new' : `edit-${String(editing)}`}
282
+ />
283
+ ) : null}
284
+
285
+ {isDuplicate ? (
286
+ <p role="alert" className="tai-field-error">
287
+ <XCircleIcon />
288
+ This combo is already added.
289
+ </p>
290
+ ) : null}
291
+
292
+ <div className="tai-row">
293
+ <Button type="button" variant="primary" onClick={commitCombo} disabled={!canAdd}>
294
+ {editing === null ? 'Add combo' : 'Update combo'}
295
+ </Button>
296
+ {editing !== null ? (
297
+ <Button type="button" onClick={resetDraft} disabled={disabled}>
298
+ Cancel edit
299
+ </Button>
300
+ ) : null}
301
+ </div>
302
+ </div>
303
+ </div>
304
+ );
305
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Pure catalog-shaping helpers for the extensions surface — the SHARED grouping
3
+ * source for both the read-only extensions catalog (`features/extensions`) and the
4
+ * `ExtensionPicker`. Kept free of React so the grouping logic is unit-tested
5
+ * directly.
6
+ *
7
+ * The `/api/extensions` catalog is a flat list of `{ name, kind }` entries. Two
8
+ * independent groupings live here:
9
+ * - `groupIntoFamilies` groups a tool's composed branch VARIANTS (which share a
10
+ * base tool name and append underscore suffixes — `tool`, `tool_a`, `tool_a_b`)
11
+ * back into one FAMILY keyed by that base name (the catalog view);
12
+ * - `groupByKind` groups the catalog by extension KIND (the picker view), where
13
+ * the `backend` kind is the single non-stackable one.
14
+ * `kindVariant` maps each serialized `kind` value to a design-system Badge variant.
15
+ */
16
+ import type { Extension } from '@tai42/api-client';
17
+
18
+ import type { BadgeProps } from './badge';
19
+
20
+ /**
21
+ * The single non-stackable extension kind: at most one `backend` extension may
22
+ * appear in a combo (the `backend` kind is declared non-stackable, `multiple=False`).
23
+ * The `ExtensionPicker` enforces single-select for this kind.
24
+ */
25
+ export const NON_STACKABLE_KIND = 'backend';
26
+
27
+ /** One base tool and every extension entry that branches from it. */
28
+ export interface ExtensionFamily {
29
+ /** The base tool name (the segment before the first underscore). */
30
+ readonly base: string;
31
+ /** The family's entries, ordered by full name. */
32
+ readonly members: readonly Extension[];
33
+ }
34
+
35
+ /** One extension `kind` and every catalog entry of that kind. */
36
+ export interface ExtensionKindGroup {
37
+ /** The serialized kind value (`wrapper` / `transformer` / `backend`). */
38
+ readonly kind: string;
39
+ /** Whether this kind is non-stackable (single-select in the picker). */
40
+ readonly nonStackable: boolean;
41
+ /** The kind's entries, ordered by name. */
42
+ readonly members: readonly Extension[];
43
+ }
44
+
45
+ /**
46
+ * The base tool name a variant belongs to: the substring before the first
47
+ * underscore, or the whole name when it has none. `tool`, `tool_a`, `tool_a_b`
48
+ * all resolve to `tool`; `x_chain` and `x_batch` both resolve to `x`.
49
+ */
50
+ export function baseNameOf(name: string): string {
51
+ const separator = name.indexOf('_');
52
+ return separator === -1 ? name : name.slice(0, separator);
53
+ }
54
+
55
+ /**
56
+ * Group a flat extension list into families keyed by base tool name. Families
57
+ * are ordered by base name and each family's members by full name, so the render
58
+ * order is stable regardless of the server's list order.
59
+ */
60
+ export function groupIntoFamilies(extensions: readonly Extension[]): ExtensionFamily[] {
61
+ const byBase = new Map<string, Extension[]>();
62
+ for (const extension of extensions) {
63
+ const base = baseNameOf(extension.name);
64
+ const members = byBase.get(base);
65
+ if (members) {
66
+ members.push(extension);
67
+ } else {
68
+ byBase.set(base, [extension]);
69
+ }
70
+ }
71
+ return [...byBase.entries()]
72
+ .map(([base, members]) => ({
73
+ base,
74
+ members: [...members].sort((a, b) => a.name.localeCompare(b.name)),
75
+ }))
76
+ .sort((a, b) => a.base.localeCompare(b.base));
77
+ }
78
+
79
+ /**
80
+ * Group a flat extension list by `kind`, for the picker. Groups are ordered by
81
+ * kind and each group's members by name, so the render order is stable regardless
82
+ * of the server's list order. The `backend` group is flagged non-stackable.
83
+ */
84
+ export function groupByKind(extensions: readonly Extension[]): ExtensionKindGroup[] {
85
+ const byKind = new Map<string, Extension[]>();
86
+ for (const extension of extensions) {
87
+ const members = byKind.get(extension.kind);
88
+ if (members) {
89
+ members.push(extension);
90
+ } else {
91
+ byKind.set(extension.kind, [extension]);
92
+ }
93
+ }
94
+ return [...byKind.entries()]
95
+ .map(([kind, members]) => ({
96
+ kind,
97
+ nonStackable: kind === NON_STACKABLE_KIND,
98
+ members: [...members].sort((a, b) => a.name.localeCompare(b.name)),
99
+ }))
100
+ .sort((a, b) => a.kind.localeCompare(b.kind));
101
+ }
102
+
103
+ /**
104
+ * Map a serialized extension `kind` to a Badge variant. Known skeleton kinds get
105
+ * a themed color; any unrecognized kind falls back to the neutral variant (the
106
+ * Badge itself also neutral-fallbacks, but choosing here keeps the mapping
107
+ * explicit and testable).
108
+ */
109
+ export function kindVariant(kind: string): NonNullable<BadgeProps['variant']> {
110
+ const known: Record<string, NonNullable<BadgeProps['variant']>> = {
111
+ wrapper: 'primary',
112
+ transformer: 'success',
113
+ backend: 'warning',
114
+ };
115
+ return known[kind] ?? 'neutral';
116
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `ExtensionPicker` — a controlled multi-select over the extension catalog
3
+ * (`GET /api/extensions`), authoring ONE extension combo (an ordered list of
4
+ * extension names). Built on the DS `Checkbox` so features share one picker
5
+ * instead of re-implementing a checklist (the same reason `SchemaForm`/`ToolPicker`
6
+ * live in the SDK). It is PRESENTATIONAL: the caller fetches `available` and owns
7
+ * the selected `value`; this component fetches nothing.
8
+ *
9
+ * The catalog is GROUPED by `kind`. The `backend` kind is the single NON-STACKABLE
10
+ * one (`multiple=False`), so it is SINGLE-SELECT — checking a backend
11
+ * extension replaces any other backend already in the combo; every other kind is
12
+ * freely multi-select. Selection order is preserved (the combo is ordered — the
13
+ * stacking order that produces the branch tool name), so a newly-checked extension
14
+ * appends to the end and unchecking removes in place.
15
+ *
16
+ * SAFETY: an extension name AND its kind are server-supplied, so every name/kind
17
+ * renders as TEXT through the DS `Checkbox`/`Badge` (React escapes it) — never an
18
+ * HTML sink. Pinned by a test.
19
+ *
20
+ * Geometry and ink come from the design-system layout classes (`tai-stack`,
21
+ * `tai-row`, `tai-muted`); the component carries no palette of its own.
22
+ */
23
+ import type { Extension } from '@tai42/api-client';
24
+
25
+ import { Badge } from './badge';
26
+ import { Checkbox } from './checkbox';
27
+ import { groupByKind, kindVariant } from './extension-grouping';
28
+
29
+ export interface ExtensionPickerProps {
30
+ /** The extension catalog (`GET /api/extensions`) the checklist is drawn from. */
31
+ readonly available: readonly Extension[];
32
+ /** The currently-selected extension names, in combo (stacking) order. */
33
+ readonly value: readonly string[];
34
+ /** Fired with the next ordered selection whenever a box toggles. */
35
+ readonly onChange: (names: string[]) => void;
36
+ readonly disabled?: boolean;
37
+ readonly idPrefix?: string;
38
+ }
39
+
40
+ export function ExtensionPicker({
41
+ available,
42
+ value,
43
+ onChange,
44
+ disabled,
45
+ idPrefix = 'extension-picker',
46
+ }: ExtensionPickerProps) {
47
+ const groups = groupByKind(available);
48
+ const selected = new Set(value);
49
+
50
+ /**
51
+ * Toggle a single extension. For a NON-STACKABLE kind (`backend`), checking one
52
+ * first strips any other selected member of that kind before appending, so the
53
+ * combo carries at most one backend; unchecking just removes it. Order is
54
+ * preserved: a newly-checked name lands at the end.
55
+ */
56
+ const toggle = (extension: Extension, nonStackable: boolean, checked: boolean): void => {
57
+ if (!checked) {
58
+ onChange(value.filter((name) => name !== extension.name));
59
+ return;
60
+ }
61
+ if (selected.has(extension.name)) return;
62
+ const siblingNames = nonStackable
63
+ ? new Set(available.filter((e) => e.kind === extension.kind).map((e) => e.name))
64
+ : undefined;
65
+ const kept =
66
+ siblingNames === undefined ? [...value] : value.filter((name) => !siblingNames.has(name));
67
+ onChange([...kept, extension.name]);
68
+ };
69
+
70
+ return (
71
+ <div data-testid={idPrefix} className="tai-stack">
72
+ {groups.map((group) => (
73
+ <div key={group.kind} className="tai-stack tai-stack-2">
74
+ {/* The kind heading is a layout row, not prose: its badge names the kind
75
+ and the single-select qualifier rides beside it as secondary text. */}
76
+ <div className="tai-row">
77
+ <Badge variant={kindVariant(group.kind)}>{group.kind}</Badge>
78
+ {group.nonStackable ? <span className="tai-muted">(single-select)</span> : null}
79
+ </div>
80
+ <div className="tai-row">
81
+ {group.members.map((extension) => (
82
+ <Checkbox
83
+ key={extension.name}
84
+ label={extension.name}
85
+ checked={selected.has(extension.name)}
86
+ disabled={disabled}
87
+ onCheckedChange={(checked) => {
88
+ toggle(extension, group.nonStackable, checked);
89
+ }}
90
+ />
91
+ ))}
92
+ </div>
93
+ </div>
94
+ ))}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `ExternalLinkButton` — an open-link action for a URL the Studio did not author
3
+ * (a registry homepage, an interaction payload's link). Only an ABSOLUTE
4
+ * `http(s)` URL is navigable here: every other spelling — another scheme, and a
5
+ * relative reference, which would be a same-origin in-app navigation the caller
6
+ * never intended — is NEUTRALIZED into plain text with no anchor and no href.
7
+ * This is an XSS pin.
8
+ *
9
+ * The check and the neutralized rendering are `Button`'s; this module chooses the
10
+ * stricter of the two link policies rather than re-implementing either.
11
+ */
12
+ import type { ReactNode } from 'react';
13
+
14
+ import {
15
+ Button,
16
+ buttonClass,
17
+ DEFAULT_BUTTON_VARIANT,
18
+ isSafeHttpUrl,
19
+ NeutralizedLink,
20
+ } from './primitives';
21
+
22
+ export { isSafeHttpUrl } from './primitives';
23
+
24
+ export interface ExternalLinkButtonProps {
25
+ readonly url: string;
26
+ readonly children?: ReactNode;
27
+ }
28
+
29
+ export function ExternalLinkButton({ url, children }: ExternalLinkButtonProps) {
30
+ const label = children ?? url;
31
+ if (!isSafeHttpUrl(url)) {
32
+ // The same classes the navigable form wears, so the two never drift apart.
33
+ return (
34
+ <NeutralizedLink className={buttonClass(DEFAULT_BUTTON_VARIANT, undefined)}>
35
+ {label}
36
+ </NeutralizedLink>
37
+ );
38
+ }
39
+ return <Button href={url}>{label}</Button>;
40
+ }