@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,489 @@
1
+ /**
2
+ * Owned DS primitives, wearing the design-system classes from
3
+ * `components.css`. These cover the shared UI states every feature drives from
4
+ * TanStack Query: loading skeletons, a shared empty state, and a loud error
5
+ * surface. Plugins consume these — never raw Tailwind utilities.
6
+ *
7
+ * `Button` is ONE implementation for both an action and a link. Given an `href`
8
+ * it renders an anchor, and that href is checked against an http/https
9
+ * allow-list: a reference beginning `/`, `?`, `#`, `./` or `../` stays in-app, an
10
+ * absolute `http://`/`https://` URL opens in a new tab with
11
+ * `rel="noopener noreferrer external"`, and everything else — another scheme
12
+ * (`javascript:`, `data:`, …), a protocol-relative `//host`, a bare `page.html` —
13
+ * is NEUTRALIZED, rendered as plain text with no href, so a hostile URL can never
14
+ * become a live navigation target. This is an XSS pin. The neutralized text names
15
+ * BOTH admitted forms, because an in-app reference is not an http(s) URL either
16
+ * and saying only "not an http(s) URL" would describe `/tools` as blocked.
17
+ *
18
+ * The check reads the NORMALIZED reference and the anchor is given that same
19
+ * normalized string, because the raw input and the URL the browser resolves are
20
+ * not the same URL. See `normalizeHref`.
21
+ */
22
+ import type {
23
+ AnchorHTMLAttributes,
24
+ ButtonHTMLAttributes,
25
+ CSSProperties,
26
+ ReactNode,
27
+ Ref,
28
+ } from 'react';
29
+
30
+ import { XCircleIcon } from './icons';
31
+
32
+ // -- Link safety --------------------------------------------------------------
33
+
34
+ /**
35
+ * The reference with the characters the URL parser throws away before it parses:
36
+ * ASCII tab, LF and CR ANYWHERE in the string, plus leading and trailing C0
37
+ * controls and spaces.
38
+ *
39
+ * Reading the raw string instead means checking a different URL from the one the
40
+ * browser resolves. `/<TAB>/evil.com` is not a root-relative path — the parser
41
+ * deletes the tab, making it the protocol-relative `//evil.com`, which is
42
+ * cross-origin.
43
+ */
44
+ function normalizeHref(href: string): string {
45
+ const inner = href.replaceAll(/[\t\n\r]/g, '');
46
+ // The leading/trailing strip walks code points rather than using a character
47
+ // class, so it covers every C0 control without a control-character regex.
48
+ let start = 0;
49
+ let end = inner.length;
50
+ while (start < end && inner.charCodeAt(start) <= 0x20) start++;
51
+ while (end > start && inner.charCodeAt(end - 1) <= 0x20) end--;
52
+ return inner.slice(start, end);
53
+ }
54
+
55
+ /**
56
+ * The absolute http(s) URL `href` denotes, or `undefined` when it denotes none.
57
+ *
58
+ * The `//` is required rather than inferred from a successful `new URL()`,
59
+ * because for a scheme that matches the document's own, the authority-less
60
+ * spelling is a PATH: an anchor with `href="https:/settings"` on an https page
61
+ * navigates to `/settings` on the CURRENT origin, while `new URL()` with no base
62
+ * reads the same string as the host `settings`. Only the `//` form means the same
63
+ * thing to both.
64
+ *
65
+ * Callers render the returned string rather than their input, so the URL that was
66
+ * judged is always the URL that is navigated.
67
+ */
68
+ export function safeHttpUrl(href: string): string | undefined {
69
+ const normalized = normalizeHref(href);
70
+ if (!/^https?:\/\//i.test(normalized)) return undefined;
71
+ try {
72
+ return new URL(normalized).href;
73
+ } catch {
74
+ return undefined;
75
+ }
76
+ }
77
+
78
+ /** True only for an absolute `http://`/`https://` URL. Everything else is unsafe. */
79
+ export function isSafeHttpUrl(url: string): boolean {
80
+ return safeHttpUrl(url) !== undefined;
81
+ }
82
+
83
+ /**
84
+ * A same-document, root-relative or explicitly relative reference. The negative
85
+ * lookahead rejects a protocol-relative `//host` (and its `/\` spelling), which
86
+ * inherits the page scheme and so resolves to a cross-origin target rather than
87
+ * an in-app one. Tested against the NORMALIZED reference — see `normalizeHref`.
88
+ *
89
+ * The lookahead belongs to the `/` branch ALONE. Applied to `#` and `?` as well
90
+ * it rejected `#/agents` — a hash route, the most common client-routed reference
91
+ * form there is, and one of the five spellings this module's own docblock says
92
+ * "stays in-app" — sending it to the neutralized span reserved for
93
+ * `javascript:`. Neither `#//evil.example` nor `?//evil.example` can be
94
+ * cross-origin: both resolve against the current document, so the lookahead
95
+ * bought nothing on those two branches and cost a whole routing style.
96
+ */
97
+ const RELATIVE_HREF = /^(?:\/(?![/\\])|[?#]|\.{1,2}\/)/;
98
+
99
+ /**
100
+ * The text a neutralized href carries in place of a navigation. It names both
101
+ * admitted forms: `/tools` and `#/agents` are in-app references rather than
102
+ * http(s) URLs and DO render as live anchors, so a reason citing only the
103
+ * http(s) rule would state a rule this component does not enforce.
104
+ */
105
+ const BLOCKED_HREF_TITLE =
106
+ 'This link was blocked because it is neither an in-app reference nor an http(s) URL.';
107
+
108
+ /**
109
+ * Takes a subtree out of the accessibility tree without taking it out of the
110
+ * layout: `display: contents` generates no box, so wrapped children stay direct
111
+ * flex items of the button surface around them.
112
+ */
113
+ const hiddenFromReadersStyle: CSSProperties = { display: 'contents' };
114
+
115
+ /**
116
+ * A blocked href, rendered as inert text: no anchor, no `href`, no handlers, so
117
+ * it can never become a live navigation target. Shared by `Button`'s link form
118
+ * and by `ExternalLinkButton`, which applies the stricter http(s)-only policy.
119
+ *
120
+ * Its surface is DELIBERATELY narrow. It takes the paint, the id and the name,
121
+ * and nothing else the caller wrote on the anchor: the props a live link needs
122
+ * are the props a dead one must not have, so they are refused here rather than
123
+ * spread onto the span.
124
+ *
125
+ * It stays ROLE-LESS on purpose — it is not a link and must not be announced as
126
+ * one. That is also why the caller's name arrives as visually-hidden TEXT rather
127
+ * than `aria-label`: ARIA prohibits both naming attributes on the `generic` role
128
+ * a bare `<span>` maps to, so a platform that honours the prohibition computes NO
129
+ * name from them and an icon-only blocked link announces as nothing at all. Text
130
+ * has no such restriction — a generic element contributes no accessible NAME, but
131
+ * its content is still read. `aria-disabled` stays because
132
+ * `.tai-btn[aria-disabled='true']` is what paints the disabled look; it is inert
133
+ * on a role-less element, and the missing `href` is what actually says this does
134
+ * not navigate.
135
+ *
136
+ * A caller-supplied `label` REPLACES the children for assistive tech, exactly as
137
+ * the `aria-label` it comes from would have done on the live anchor: the children
138
+ * go behind an `aria-hidden` wrapper that is `display: contents`, so it generates
139
+ * no box and the button's own flex layout is unchanged. Without that the children
140
+ * and the label are both read and a blocked link announces its name twice.
141
+ */
142
+ export function NeutralizedLink({
143
+ className,
144
+ style,
145
+ children,
146
+ id,
147
+ label,
148
+ }: {
149
+ readonly className?: string;
150
+ readonly style?: CSSProperties;
151
+ readonly children?: ReactNode;
152
+ /** Kept so an external `aria-labelledby`/`aria-describedby` IDREF still lands. */
153
+ readonly id?: string;
154
+ /**
155
+ * The name the caller gave the link, rendered as hidden text. An icon-only
156
+ * link carries its whole meaning here, and when it is given the children are
157
+ * hidden from assistive tech so the name is read once, not twice.
158
+ */
159
+ readonly label?: string;
160
+ }) {
161
+ return (
162
+ <span
163
+ data-neutralized="true"
164
+ aria-disabled="true"
165
+ title={BLOCKED_HREF_TITLE}
166
+ className={className}
167
+ style={style}
168
+ id={id}
169
+ >
170
+ {label === undefined ? (
171
+ children
172
+ ) : (
173
+ <span aria-hidden="true" style={hiddenFromReadersStyle}>
174
+ {children}
175
+ </span>
176
+ )}
177
+ {/* `title` on a non-focusable span is not reliably announced, so the name
178
+ and the reason the link is dead are carried as real text. An icon-only
179
+ link would otherwise neutralize into a silent nothing. */}
180
+ <span className="tai-visually-hidden">
181
+ {label === undefined ? BLOCKED_HREF_TITLE : `${label}. ${BLOCKED_HREF_TITLE}`}
182
+ </span>
183
+ </span>
184
+ );
185
+ }
186
+
187
+ interface ResolvedHref {
188
+ readonly kind: 'internal' | 'external' | 'blocked';
189
+ /** The reference to put in the anchor: exactly the one that was judged. */
190
+ readonly href: string;
191
+ }
192
+
193
+ /**
194
+ * Sorts an href into the three link forms the button renders, and returns the
195
+ * normalized reference alongside. Rendering the caller's raw string instead
196
+ * would let the anchor navigate somewhere the check never saw.
197
+ */
198
+ function resolveHref(href: string): ResolvedHref {
199
+ const external = safeHttpUrl(href);
200
+ if (external !== undefined) return { kind: 'external', href: external };
201
+ const normalized = normalizeHref(href);
202
+ if (RELATIVE_HREF.test(normalized)) return { kind: 'internal', href: normalized };
203
+ return { kind: 'blocked', href: '' };
204
+ }
205
+
206
+ // -- Shared surface props -----------------------------------------------------
207
+
208
+ /**
209
+ * What every primitive in this file accepts alongside its own props: the
210
+ * design-system class it wears is its own, and the caller's `className` is
211
+ * APPENDED to it rather than replacing it, so a surface can never lose its paint
212
+ * by being positioned.
213
+ */
214
+ interface SurfaceProps {
215
+ readonly className?: string;
216
+ readonly style?: CSSProperties;
217
+ }
218
+
219
+ /** The surface's own class plus the caller's, which sorts last so it can override. */
220
+ function surfaceClass(base: string, className: string | undefined): string {
221
+ return className === undefined ? base : `${base} ${className}`;
222
+ }
223
+
224
+ // -- Button ------------------------------------------------------------------
225
+
226
+ export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
227
+
228
+ /** The variant a `Button` wears when the caller names none. */
229
+ export const DEFAULT_BUTTON_VARIANT: ButtonVariant = 'secondary';
230
+
231
+ const VARIANT_CLASS: Record<ButtonVariant, string> = {
232
+ primary: 'tai-btn tai-btn-primary',
233
+ secondary: 'tai-btn tai-btn-secondary',
234
+ ghost: 'tai-btn tai-btn-ghost',
235
+ danger: 'tai-btn tai-btn-danger',
236
+ };
237
+
238
+ /**
239
+ * The one prop both forms share. Deliberately NOT on the public surface: it is a
240
+ * shared base, and declaration emit keeps it visible inside `primitives.d.ts`
241
+ * where `ButtonProps`/`LinkButtonProps` reference it, so consumers still see
242
+ * `variant` and can still `extends` either interface.
243
+ */
244
+ interface ButtonVariantProps {
245
+ readonly variant?: ButtonVariant;
246
+ }
247
+
248
+ /**
249
+ * The ACTION form of `Button`: button attributes, no anchor ones.
250
+ *
251
+ * This stays an INTERFACE extending `ButtonHTMLAttributes<HTMLButtonElement>`
252
+ * because it is published plugin API: a plugin may write
253
+ * `interface MyButton extends ButtonProps {}` (a union cannot be extended —
254
+ * TS2312) and may hand a `ButtonProps` value to a slot typed as plain
255
+ * `ButtonHTMLAttributes<HTMLButtonElement>` (a union including anchor attributes
256
+ * is not assignable — TS2322). The link form is the ADDITIVE `LinkButtonProps`,
257
+ * and `Button` accepts either.
258
+ *
259
+ * It declares NO `href` member. `href?: undefined` here would make
260
+ * `interface MyButton extends ButtonProps { href: string }` a TS2430
261
+ * ("incorrectly extends"), which is a narrowing of a surface that is additive
262
+ * only. The discriminant `Button` narrows on lives on `ButtonActionProps`.
263
+ */
264
+ export interface ButtonProps extends ButtonVariantProps, ButtonHTMLAttributes<HTMLButtonElement> {
265
+ /** A consumer ref for the `<button>`; the prop spread forwards it. */
266
+ readonly ref?: Ref<HTMLButtonElement>;
267
+ }
268
+
269
+ /**
270
+ * The action form as `Button` ACCEPTS it: `ButtonProps` plus the absent-`href`
271
+ * discriminant, which is what lets the body narrow `props.href` without a cast.
272
+ *
273
+ * Like `ButtonVariantProps` this is deliberately off the public surface — a
274
+ * plugin extends `ButtonProps` — and declaration emit keeps it visible inside
275
+ * `primitives.d.ts`, where `Button`'s signature references it.
276
+ */
277
+ interface ButtonActionProps extends ButtonProps {
278
+ readonly href?: undefined;
279
+ }
280
+
281
+ /**
282
+ * The LINK form of `Button`: an `href`, so it carries anchor attributes and no
283
+ * button ones.
284
+ *
285
+ * `target` and `rel` are inherited from `AnchorHTMLAttributes` and are settable,
286
+ * but on the EXTERNAL branch they are not the caller's to choose: an absolute
287
+ * http(s) URL always renders `target="_blank"` with
288
+ * `rel="noopener noreferrer external"`, whatever the caller passed. On the
289
+ * in-app branch `target` is the caller's and `rel` is pinned only when they ask
290
+ * for `_blank`.
291
+ *
292
+ * It declares NO `ref`, and that is a REFUSAL rather than an omission: a blocked
293
+ * href renders a {@link NeutralizedLink} span and no anchor at all, so a
294
+ * `Ref<HTMLAnchorElement>` would be filled on two of the three renderings and
295
+ * left null on the third — a ref that silently does nothing for exactly the
296
+ * input the check exists to catch. A caller that needs the element wraps the
297
+ * button.
298
+ */
299
+ export interface LinkButtonProps
300
+ extends ButtonVariantProps, AnchorHTMLAttributes<HTMLAnchorElement> {
301
+ readonly href: string;
302
+ }
303
+
304
+ /** The variant class plus the caller's, which sorts last so it can override. */
305
+ export function buttonClass(variant: ButtonVariant, className: string | undefined): string {
306
+ return surfaceClass(VARIANT_CLASS[variant], className);
307
+ }
308
+
309
+ export function Button(props: ButtonActionProps | LinkButtonProps) {
310
+ if (props.href === undefined) {
311
+ const { variant = DEFAULT_BUTTON_VARIANT, className, href: _href, ...rest } = props;
312
+ return <button {...rest} className={buttonClass(variant, className)} />;
313
+ }
314
+
315
+ const { variant = DEFAULT_BUTTON_VARIANT, className, style, href, children, ...rest } = props;
316
+ const classes = buttonClass(variant, className);
317
+ const link = resolveHref(href);
318
+
319
+ if (link.kind === 'blocked') {
320
+ // Only the four props that survive the neutralization cross: the paint, the
321
+ // id an external IDREF may point at, and the caller's name (as text). Every
322
+ // other anchor prop is REFUSED rather than forwarded — `target`/`rel`
323
+ // describe a navigation that is not happening, and a handler spread onto the
324
+ // span would make the dead link live again, which is the whole defect.
325
+ return (
326
+ <NeutralizedLink className={classes} style={style} id={rest.id} label={rest['aria-label']}>
327
+ {children}
328
+ </NeutralizedLink>
329
+ );
330
+ }
331
+
332
+ if (link.kind === 'internal') {
333
+ return (
334
+ <a
335
+ {...rest}
336
+ href={link.href}
337
+ // An in-app link is same-origin, but a caller may still ask for a new
338
+ // tab; `rel` is pinned after the spread so that ask cannot drop it.
339
+ {...(rest.target === '_blank' ? { rel: 'noopener noreferrer' } : {})}
340
+ className={classes}
341
+ style={style}
342
+ >
343
+ {children}
344
+ </a>
345
+ );
346
+ }
347
+
348
+ return (
349
+ <a
350
+ {...rest}
351
+ // REVERSE TABNABBING. `target`/`rel` are settable on `LinkButtonProps`, so
352
+ // the hardening is carried by JSX property ORDER alone: the spread comes
353
+ // FIRST and these two are written after it, which is what makes them
354
+ // unoverridable. Below the spread, `rel="opener" target="_self"` from a
355
+ // caller would open a cross-origin `_blank` with a live `window.opener`
356
+ // onto the operator's authenticated tab.
357
+ href={link.href}
358
+ target="_blank"
359
+ rel="noopener noreferrer external"
360
+ className={classes}
361
+ style={style}
362
+ >
363
+ {children}
364
+ </a>
365
+ );
366
+ }
367
+
368
+ // -- Card --------------------------------------------------------------------
369
+
370
+ export interface CardProps extends SurfaceProps {
371
+ readonly children: ReactNode;
372
+ /** Opt in to the hover/focus lift. A card that is not itself an affordance stays flat. */
373
+ readonly interactive?: boolean;
374
+ /** A consumer ref for the card element itself. */
375
+ readonly ref?: Ref<HTMLDivElement>;
376
+ }
377
+
378
+ export function Card({ children, interactive = false, className, style, ref }: CardProps) {
379
+ const base = interactive ? 'tai-card tai-card-interactive' : 'tai-card';
380
+ return (
381
+ <div className={surfaceClass(base, className)} style={style} ref={ref}>
382
+ {children}
383
+ </div>
384
+ );
385
+ }
386
+
387
+ // -- Skeleton (loading) ------------------------------------------------------
388
+
389
+ export interface SkeletonProps extends SurfaceProps {
390
+ readonly height?: number | string;
391
+ readonly width?: number | string;
392
+ }
393
+
394
+ export function Skeleton({ height = 16, width = '100%', className, style }: SkeletonProps) {
395
+ // The block's extent is per-instance; the sheen animation lives in the class.
396
+ return (
397
+ <div
398
+ aria-hidden="true"
399
+ className={surfaceClass('tai-skeleton', className)}
400
+ style={{ height, width, ...style }}
401
+ />
402
+ );
403
+ }
404
+
405
+ // -- EmptyState --------------------------------------------------------------
406
+
407
+ export interface EmptyStateProps extends SurfaceProps {
408
+ readonly title: string;
409
+ readonly description?: string;
410
+ /** The guidance line's on-screen action — the control the description names. */
411
+ readonly action?: ReactNode;
412
+ }
413
+
414
+ export function EmptyState({ title, description, action, className, style }: EmptyStateProps) {
415
+ return (
416
+ <div role="status" className={surfaceClass('tai-empty-state', className)} style={style}>
417
+ <p className="tai-empty-state-title">{title}</p>
418
+ {description === undefined ? null : (
419
+ <p style={{ margin: 'var(--tai-space-2) 0 0' }}>{description}</p>
420
+ )}
421
+ {action === undefined ? null : (
422
+ <div style={{ marginTop: 'var(--tai-space-4)' }}>{action}</div>
423
+ )}
424
+ </div>
425
+ );
426
+ }
427
+
428
+ // -- ErrorState (loud, visible) ----------------------------------------------
429
+
430
+ /**
431
+ * The headline every `ErrorState` wears.
432
+ *
433
+ * It is fixed, and the whole surface says one thing with it: `role="alert"`
434
+ * interrupts, the crossed circle is the ERROR mark, and `.tai-error-state` is the
435
+ * error ground. A DELIBERATE refusal — a permission denial, a server's dry-run
436
+ * "no" — is an answer rather than a malfunction, and swapping this headline alone
437
+ * would leave the mark, the ground and the live-region politeness all still
438
+ * announcing a system failure. Such a surface takes the design system's warn
439
+ * ground instead — `.tai-warn-state` for a warning block (`advisories.tsx`,
440
+ * `fleet-report.tsx`, `RunPanel.tsx`), or `role="status"` with a warn `Badge` for
441
+ * a verdict that only lands after the reader acts (`verdict.tsx`). The live-region
442
+ * politeness — `role="alert"` for a warning that interrupts, `role="status"` for
443
+ * one the reader came to find — is the surface's to choose, not this headline's.
444
+ */
445
+ const ERROR_TITLE = 'Something went wrong';
446
+
447
+ export interface ErrorStateProps extends SurfaceProps {
448
+ readonly message: string;
449
+ readonly onRetry?: () => void;
450
+ }
451
+
452
+ export function ErrorState({ message, onRetry, className, style }: ErrorStateProps) {
453
+ return (
454
+ <div role="alert" className={surfaceClass('tai-error-state', className)} style={style}>
455
+ {/* The icon carries the state alongside the color, never the color alone,
456
+ and it is the ERROR mark: the crossed circle. The warning triangle is
457
+ the warn surfaces' mark and would state a second severity here. */}
458
+ <strong className="tai-error-state-title">
459
+ <XCircleIcon />
460
+ {ERROR_TITLE}
461
+ </strong>
462
+ <p style={{ margin: 'var(--tai-space-2) 0 0', whiteSpace: 'pre-wrap' }}>{message}</p>
463
+ {onRetry === undefined ? null : (
464
+ <div style={{ marginTop: 'var(--tai-space-3)' }}>
465
+ <Button type="button" onClick={onRetry}>
466
+ Retry
467
+ </Button>
468
+ </div>
469
+ )}
470
+ </div>
471
+ );
472
+ }
473
+
474
+ // -- Spinner -----------------------------------------------------------------
475
+
476
+ export interface SpinnerProps extends SurfaceProps {
477
+ readonly label?: string;
478
+ }
479
+
480
+ export function Spinner({ label = 'Loading', className, style }: SpinnerProps) {
481
+ return (
482
+ <span
483
+ role="status"
484
+ aria-label={label}
485
+ className={surfaceClass('tai-spinner', className)}
486
+ style={style}
487
+ />
488
+ );
489
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `ProgressBar` — a design-system, accessible progress indicator for a running
3
+ * tool that reports progress (`ctx.report_progress`). Determinate when a
4
+ * positive `total` is known (fills to `value/total`); otherwise indeterminate.
5
+ *
6
+ * The track and fill are classes; only the determinate fill's WIDTH stays inline,
7
+ * because it is the one genuinely per-instance value here. The indeterminate
8
+ * sweep is `tai-progress-fill-indeterminate`.
9
+ *
10
+ * Reusable by any feature's run surface — it takes only the raw
11
+ * progress/total/message a `report_progress` notification carries and renders
12
+ * a native `role="progressbar"` element with the ARIA value fields set.
13
+ */
14
+ import type { CSSProperties, ReactNode } from 'react';
15
+
16
+ export interface ProgressBarProps {
17
+ /** Current progress. Clamped to `[0, total]` when `total` is known. */
18
+ readonly value?: number;
19
+ /** Upper bound. When absent or non-positive the bar is INDETERMINATE. */
20
+ readonly total?: number;
21
+ /** Optional human-readable status shown alongside the bar. */
22
+ readonly message?: string;
23
+ }
24
+
25
+ function clampFraction(value: number, total: number): number {
26
+ if (!(total > 0)) return 0;
27
+ const fraction = value / total;
28
+ // Written as `>= 0` rather than `< 0` so that NaN — which compares false to
29
+ // everything, and would otherwise reach the width and the label as `NaN%` —
30
+ // falls through to the floor.
31
+ if (!(fraction >= 0)) return 0;
32
+ if (fraction > 1) return 1;
33
+ return fraction;
34
+ }
35
+
36
+ /**
37
+ * The value to ANNOUNCE, clamped into `[0, total]`. Derived from `value`
38
+ * directly rather than by multiplying the fraction back up: `value / total *
39
+ * total` does not round-trip in binary floating point, so `value={7}
40
+ * total={25}` announced `7.000000000000001`, and a non-finite `total` announced
41
+ * `NaN`. The same `>= 0` shape as `clampFraction` keeps NaN at the floor.
42
+ */
43
+ function clampValue(value: number, total: number): number {
44
+ if (!(value >= 0)) return 0;
45
+ if (!(value <= total)) return total;
46
+ return value;
47
+ }
48
+
49
+ /** The status sits at one end of the row and the percentage at the other. */
50
+ const labelRowStyle: CSSProperties = { justifyContent: 'space-between' };
51
+
52
+ /**
53
+ * Render a progress bar. With a positive `total`, `value/total` fills the track
54
+ * and the percentage is announced via `aria-valuenow`; without one, the bar is
55
+ * indeterminate (no `aria-valuenow`, per ARIA).
56
+ */
57
+ export function ProgressBar({ value = 0, total, message }: ProgressBarProps): ReactNode {
58
+ // A determinate bar has to announce a real `aria-valuemax`. `Number.isFinite`
59
+ // rather than `> 0` alone: `total={Infinity}` passes the positivity test but
60
+ // announces `aria-valuemax="Infinity"`, which is not a number any assistive
61
+ // technology can place `aria-valuenow` against, and draws a 0 % fill while
62
+ // announcing the raw value. An unbounded total is exactly the indeterminate
63
+ // case, so it renders as one.
64
+ const determinate = typeof total === 'number' && Number.isFinite(total) && total > 0;
65
+ const fraction = determinate ? clampFraction(value, total) : undefined;
66
+ const percent = fraction === undefined ? undefined : Math.round(fraction * 100);
67
+
68
+ return (
69
+ <div className="tai-stack tai-stack-2">
70
+ {/* The status line always renders. An indeterminate bar under reduced
71
+ motion is a full, still track — visually identical to a finished one —
72
+ so the only thing separating "in flight" from "done" for a sighted
73
+ reader is this line saying so. */}
74
+ <div className="tai-row tai-field-hint" style={labelRowStyle}>
75
+ <span>{message ?? 'Working…'}</span>
76
+ {percent !== undefined && <span>{percent}%</span>}
77
+ </div>
78
+ <div
79
+ role="progressbar"
80
+ className="tai-progress-track"
81
+ aria-valuemin={0}
82
+ aria-valuemax={determinate ? total : undefined}
83
+ // The clamped value, not the raw prop: a bar drawn full while it
84
+ // announces `aria-valuenow="99"` against `aria-valuemax="10"` tells a
85
+ // screen-reader user something the screen does not.
86
+ aria-valuenow={determinate && fraction !== undefined ? clampValue(value, total) : undefined}
87
+ aria-label={message ?? 'Progress'}
88
+ >
89
+ {determinate ? (
90
+ <div
91
+ className="tai-progress-fill"
92
+ style={{ width: `${String((fraction ?? 0) * 100)}%` }}
93
+ />
94
+ ) : (
95
+ <div className="tai-progress-fill tai-progress-fill-indeterminate" />
96
+ )}
97
+ </div>
98
+ </div>
99
+ );
100
+ }