@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,2212 @@
1
+ /*
2
+ * The design-system COMPONENT + LAYOUT layer: the real classes every Studio
3
+ * surface is built from. It is the only place in the monorepo that may ship
4
+ * COMPONENT CSS — `tokens.css` and `fonts.css` are the design system's other two
5
+ * published sheets, and `apps/studio/src/styles.css` is the shell's own entry;
6
+ * feature packages compile with `tsc` and `sideEffects: false`, so a stylesheet
7
+ * inside one would never reach a bundle. Screens therefore apply these classes BY
8
+ * NAME rather than re-deriving geometry inline.
9
+ *
10
+ * Everything resolves from `tokens.css`; there are no literal colors, radii, or
11
+ * shadows below.
12
+ *
13
+ * THE FIVE-STATE CONTRACT, enforced off this sheet by `sheet-contract.test.ts`.
14
+ * A class that presents itself as a control — it declares `cursor: pointer` at
15
+ * rest — carries every one of:
16
+ * rest its own rule;
17
+ * hover a rule that fires while THAT class is hovered — the response
18
+ * may land on a child, which is what a label does for its box;
19
+ * active a press rendering, required of a control whose hover MOVES it
20
+ * and of no other: `.tai-btn` lifts, so it has to come back
21
+ * down, and nothing else in this sheet moves under the pointer;
22
+ * unavailable a `:disabled` / `[aria-disabled]` / `[data-disabled]` rule;
23
+ * focus-visible membership of the one shared ring rule below.
24
+ * The gate carries the exemptions by name with a reason each, so the next
25
+ * omission is a failure rather than a silent gap. Focus is a 2 px outline at a
26
+ * 2 px offset, never a box-shadow, because an outline follows `border-radius`
27
+ * and takes no part in layout.
28
+ *
29
+ * Breakpoints, desktop-first with real mobile: <640 phone, 640-1023 compact,
30
+ * 1024-1279 medium, >=1280 full.
31
+ */
32
+ @layer tai-components {
33
+ /* -- Global typography ---------------------------------------------------
34
+ Headings are black and tight in every context; the host preflight resets
35
+ their size and weight, and this layer sorts after it. Sizes follow the
36
+ scale so an unclassed heading is still on-system; a component class or an
37
+ explicit style overrides them.
38
+
39
+ No `font-feature-settings`. None of the thirteen `.woff2` faces this design
40
+ system ships carries a character-variant or stylistic-set feature at all —
41
+ the Inter latin subset's whole feature list is `calt ccmp dnom frac locl
42
+ numr pnum tnum` — so naming one selects nothing and only reads as a spec
43
+ that is being met. The gate holds the sheet to that. */
44
+ h1,
45
+ h2,
46
+ h3,
47
+ h4,
48
+ h5,
49
+ h6 {
50
+ color: var(--tai-color-heading);
51
+ font-family: var(--tai-font-sans);
52
+ font-weight: 700;
53
+ letter-spacing: -0.03em;
54
+ }
55
+
56
+ h1 {
57
+ font-size: var(--tai-text-xl);
58
+ line-height: 1.2;
59
+ }
60
+
61
+ h2 {
62
+ font-size: var(--tai-text-section);
63
+ line-height: 1.25;
64
+ }
65
+
66
+ h3 {
67
+ font-size: var(--tai-text-lg);
68
+ line-height: 1.3;
69
+ font-weight: 600;
70
+ }
71
+
72
+ h4,
73
+ h5,
74
+ h6 {
75
+ font-size: var(--tai-text-md);
76
+ line-height: 1.4;
77
+ font-weight: 600;
78
+ }
79
+
80
+ /* A real selection pair, not the near-background accent TINT: the tint is the
81
+ fill behind a selected nav item, and over body copy it is 1.13:1 (light) /
82
+ 1.11:1 (dark) against the unselected background — dragging over prose gives
83
+ essentially no feedback about what had been selected.
84
+
85
+ Both halves of the pair are declared, so the selected run's own contrast is
86
+ `on-accent` over `accent` and nothing else: derived from the token hex
87
+ values, 4.99:1 in light (#ffffff on #dc143c) and 5.36:1 in dark (#0c0e12 on
88
+ #ed4c67). Both clear the 4.5:1 body-text floor, and the accent ground is the
89
+ same 4.99:1 / 5.36:1 away from the page ground, so the selection is visible
90
+ as well as legible. */
91
+ ::selection {
92
+ background: var(--tai-color-accent);
93
+ color: var(--tai-color-on-accent);
94
+ }
95
+
96
+ /* Chromium paints its own opaque autofill background, which reads as a light
97
+ patch inside a dark form. Repaint it from the surface token and force the
98
+ text color to match. */
99
+ input:-webkit-autofill,
100
+ input:-webkit-autofill:hover,
101
+ input:-webkit-autofill:focus,
102
+ textarea:-webkit-autofill {
103
+ -webkit-text-fill-color: var(--tai-color-text);
104
+ -webkit-box-shadow: 0 0 0 100px var(--tai-color-surface-raised) inset;
105
+ caret-color: var(--tai-color-text);
106
+ }
107
+
108
+ /* -- The uppercase mono label ---------------------------------------------
109
+ ONE owner for the 11 px tracked-mono label style: nav section headers,
110
+ table headers, page eyebrows, chart axis labels. Never re-derived inline.
111
+ `.tai-nav-section-header` is the shell's existing name for the same style;
112
+ `.tai-table th` is here because a header cell in a table this repo renders
113
+ cannot carry a class. Prose tables are NOT in this set: their header text is
114
+ authored elsewhere, and shrinking it below its own body cells (11 px against
115
+ 13.5 px) would invert the table's hierarchy — `.tai-prose th` styles itself. */
116
+ .tai-label,
117
+ .tai-nav-section-header,
118
+ .tai-table th {
119
+ font-family: var(--tai-font-mono);
120
+ font-size: var(--tai-text-xs);
121
+ font-weight: 500;
122
+ line-height: 1.4;
123
+ letter-spacing: 0.13em;
124
+ text-transform: uppercase;
125
+ color: var(--tai-color-text-muted);
126
+ }
127
+
128
+ .tai-nav-section-header {
129
+ padding: var(--tai-space-4) var(--tai-space-3) var(--tai-space-2);
130
+ }
131
+
132
+ /* -- Shared focus ---------------------------------------------------------
133
+ An outline (not a box-shadow) at a 2 px offset: an outline follows the
134
+ element's `border-radius` and takes no part in layout, so the ring never
135
+ shifts what it surrounds.
136
+
137
+ It is NOT immune to clipping. An outline is painted in the element's own box
138
+ and is cut by an ancestor's overflow box exactly as a box-shadow is, so a
139
+ scrolling ancestor has to leave the ring room or it cuts every ring inside it
140
+ down to the two segments that fit (see `.tai-select-viewport`). */
141
+ .tai-btn:focus-visible,
142
+ .tai-icon-btn:focus-visible,
143
+ .tai-link:focus-visible,
144
+ .tai-input:focus-visible,
145
+ .tai-textarea:focus-visible,
146
+ .tai-select-trigger:focus-visible,
147
+ .tai-select-item:focus-visible,
148
+ .tai-checkbox:focus-visible,
149
+ .tai-radio:focus-visible,
150
+ .tai-segment:focus-visible,
151
+ .tai-chip:focus-visible,
152
+ .tai-tab:focus-visible,
153
+ .tai-nav-item:focus-visible,
154
+ .tai-nav-link:focus-visible,
155
+ /* The brand lockup is a link home in every shell that renders it — it declares
156
+ link affordances (`text-decoration: none` over the heading ink) and sits in
157
+ the sidebar's tab order ahead of the nav, so it needs the ring like any
158
+ other chrome link. */
159
+ .tai-brand:focus-visible,
160
+ .tai-skip-link:focus-visible,
161
+ .tai-card-interactive:focus-visible,
162
+ .tai-scroll-region:focus-visible,
163
+ .tai-code-block:focus-visible,
164
+ .tai-prose a:focus-visible,
165
+ .tai-prose pre:focus-visible,
166
+ .tai-drawer:focus-visible,
167
+ /* Radix hard-codes `tabIndex: 0` on every `TabsContent`, so a tab panel is a
168
+ tab stop whether or not it holds one — it needs the ring like any other. */
169
+ .tai-tabpanel:focus-visible,
170
+ .tai-dialog:focus-visible {
171
+ outline: 2px solid var(--tai-color-focus-ring);
172
+ outline-offset: 2px;
173
+ }
174
+
175
+ /* The input's own boundary is the focus indicator's partner: it strengthens
176
+ to the accent so the field reads as active even at 200 % zoom. */
177
+ .tai-input:focus-visible,
178
+ .tai-textarea:focus-visible,
179
+ .tai-select-trigger:focus-visible {
180
+ border-color: var(--tai-color-accent);
181
+ }
182
+
183
+ /* -- Buttons --------------------------------------------------------------
184
+ One 36 px geometry for every control; the 36 px is a FLOOR and the label
185
+ sits on a line box inside it. Hover lifts 2 px; a coarse pointer RAISES that
186
+ floor to a 44 px target (the pointer-target band below wins on touch) and
187
+ leaves the lift alone — a lift is painted only while `:hover` holds, and the
188
+ three rules that do cancel it are `:disabled`, reduced motion, and `:active`.
189
+
190
+ The line box is 1.25, not 1. A caller's label may WRAP here — that is the
191
+ whole point of the `overflow-wrap` below — and a unitless line-height of 1
192
+ makes a line box SHORTER than the font's own content area, so consecutive
193
+ wrapped lines overlap. At 1.25 a single-line label still costs
194
+ 13.5 x 1.25 + 16 padding + 2 border = 34.9 px, under the 36 px floor, so
195
+ nothing about the resting control moves. */
196
+ .tai-btn {
197
+ display: inline-flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ gap: var(--tai-space-2);
201
+ box-sizing: border-box;
202
+ min-height: var(--tai-control-height);
203
+ padding: var(--tai-space-2) var(--tai-space-4);
204
+ border: 1px solid transparent;
205
+ border-radius: var(--tai-radius-md);
206
+ font-family: var(--tai-font-sans);
207
+ font-size: var(--tai-text-md);
208
+ font-weight: 500;
209
+ line-height: 1.25;
210
+ text-decoration: none;
211
+ cursor: pointer;
212
+ /* A flex item's default `min-width: auto` refuses to shrink below its own
213
+ content, so caller text — a server label, a tag, an operator's own
214
+ string — pushes the document instead of breaking inside the control.
215
+ Measured at 320 px: without this the rule pushes a document scrollbar.
216
+ `flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
217
+ because the overflow is the control's own text, not its item order. */
218
+ min-width: 0;
219
+ overflow-wrap: anywhere;
220
+ transition:
221
+ background-color var(--tai-motion-fast) ease,
222
+ border-color var(--tai-motion-fast) ease,
223
+ color var(--tai-motion-fast) ease,
224
+ box-shadow var(--tai-motion-fast) ease,
225
+ transform var(--tai-motion-fast) ease;
226
+ }
227
+
228
+ .tai-btn:hover:not(:disabled):not([aria-disabled='true']) {
229
+ transform: translateY(-2px);
230
+ box-shadow: var(--tai-shadow-lift);
231
+ }
232
+
233
+ .tai-btn:active:not(:disabled):not([aria-disabled='true']) {
234
+ transform: translateY(0);
235
+ box-shadow: none;
236
+ }
237
+
238
+ .tai-btn:disabled,
239
+ .tai-btn[aria-disabled='true'] {
240
+ background: var(--tai-color-surface-disabled);
241
+ color: var(--tai-color-text-disabled);
242
+ border-color: var(--tai-color-border-disabled);
243
+ box-shadow: none;
244
+ transform: none;
245
+ cursor: not-allowed;
246
+ }
247
+
248
+ .tai-btn-primary {
249
+ background: var(--tai-color-accent);
250
+ border-color: var(--tai-color-accent);
251
+ color: var(--tai-color-on-accent);
252
+ }
253
+
254
+ .tai-btn-primary:hover:not(:disabled):not([aria-disabled='true']) {
255
+ background: var(--tai-color-accent-hover);
256
+ border-color: var(--tai-color-accent-hover);
257
+ }
258
+
259
+ .tai-btn-secondary {
260
+ background: var(--tai-color-surface-raised);
261
+ border-color: var(--tai-color-control-border);
262
+ color: var(--tai-color-text);
263
+ }
264
+
265
+ .tai-btn-secondary:hover:not(:disabled):not([aria-disabled='true']) {
266
+ background: var(--tai-color-surface);
267
+ border-color: var(--tai-color-text);
268
+ }
269
+
270
+ .tai-btn-ghost {
271
+ background: transparent;
272
+ border-color: transparent;
273
+ color: var(--tai-color-text-muted);
274
+ }
275
+
276
+ .tai-btn-ghost:hover:not(:disabled):not([aria-disabled='true']) {
277
+ background: var(--tai-color-surface);
278
+ color: var(--tai-color-text);
279
+ }
280
+
281
+ .tai-btn-danger {
282
+ background: var(--tai-color-danger);
283
+ border-color: var(--tai-color-danger);
284
+ color: var(--tai-color-danger-text);
285
+ }
286
+
287
+ .tai-btn-danger:hover:not(:disabled):not([aria-disabled='true']) {
288
+ background: var(--tai-color-danger-hover);
289
+ border-color: var(--tai-color-danger-hover);
290
+ }
291
+
292
+ /* A square button whose only content is a 16 px icon; its accessible name
293
+ comes from the consumer's aria-label. */
294
+ .tai-icon-btn {
295
+ display: inline-flex;
296
+ align-items: center;
297
+ justify-content: center;
298
+ box-sizing: border-box;
299
+ width: var(--tai-control-height);
300
+ height: var(--tai-control-height);
301
+ padding: 0;
302
+ border: 1px solid transparent;
303
+ border-radius: var(--tai-radius-md);
304
+ background: transparent;
305
+ color: var(--tai-color-text-muted);
306
+ cursor: pointer;
307
+ transition:
308
+ background-color var(--tai-motion-fast) ease,
309
+ color var(--tai-motion-fast) ease;
310
+ }
311
+
312
+ .tai-icon-btn:hover:not(:disabled) {
313
+ background: var(--tai-color-surface);
314
+ color: var(--tai-color-text);
315
+ }
316
+
317
+ .tai-icon-btn:disabled {
318
+ color: var(--tai-color-text-disabled);
319
+ cursor: not-allowed;
320
+ }
321
+
322
+ /* -- Links ---------------------------------------------------------------- */
323
+ .tai-link {
324
+ color: var(--tai-color-accent);
325
+ text-decoration: underline;
326
+ text-underline-offset: 2px;
327
+ border-radius: var(--tai-radius-sm);
328
+ }
329
+
330
+ .tai-link:hover {
331
+ color: var(--tai-color-accent-hover);
332
+ }
333
+
334
+ /* -- Icons ----------------------------------------------------------------
335
+ Rendered at 16 px in currentColor at full opacity; a 24-unit viewBox and a
336
+ 1.6 stroke keep the whole set optically consistent. */
337
+ .tai-icon {
338
+ display: inline-block;
339
+ flex: none;
340
+ width: 16px;
341
+ height: 16px;
342
+ vertical-align: -0.15em;
343
+ }
344
+
345
+ /* A mark inside a control keeps its size while the TEXT beside it takes the
346
+ shrinking. Every row flex below declares a `min-width`, which is what tells
347
+ the layout it may shrink past its own content; the text has
348
+ `overflow-wrap: anywhere` to survive that, an svg has nothing, so it is
349
+ squashed instead — measured at 320 px, a 16 px mark inside `.tai-status`
350
+ came out 11.34 px wide. `.tai-icon` already carries `flex: none`, so what
351
+ this reaches is the svg a CONSUMER brings. The group is reconciled against
352
+ the sheet's row-flex-with-a-min-width rules in both directions by
353
+ `sheet-contract.test.ts`, so a new one cannot be silently left out. */
354
+ .tai-badge > svg,
355
+ .tai-btn > svg,
356
+ .tai-chip > svg,
357
+ .tai-choice > svg,
358
+ .tai-drawer-header > svg,
359
+ .tai-error-state-title > svg,
360
+ .tai-field-error > svg,
361
+ .tai-nav-item > svg,
362
+ .tai-nav-link > svg,
363
+ .tai-segment > svg,
364
+ .tai-select-trigger > svg,
365
+ .tai-status > svg {
366
+ flex: none;
367
+ }
368
+
369
+ /* -- Fields, inputs, selects ----------------------------------------------
370
+ `.tai-field-group` is the container a group Field renders around its
371
+ children so the group has a `role` to carry its own name and description. It
372
+ has to reproduce `.tai-field`'s formatting context exactly or wrapping would
373
+ not be layout-neutral — the children would become items of the wrapper's
374
+ default `block` layout and lose the field's gap — so it is the SAME rule
375
+ rather than a copy that can drift. A copy carrying its own `min-width: 0`
376
+ was a measured no-op: the automatic minimum size applies on the MAIN axis,
377
+ and in a column flex container that is the block axis. */
378
+ .tai-field,
379
+ .tai-field-group {
380
+ display: flex;
381
+ flex-direction: column;
382
+ gap: var(--tai-space-2);
383
+ }
384
+
385
+ .tai-field-label {
386
+ font-family: var(--tai-font-sans);
387
+ font-size: var(--tai-text-sm);
388
+ font-weight: 600;
389
+ line-height: 1.4;
390
+ color: var(--tai-color-text);
391
+ }
392
+
393
+ .tai-field-hint {
394
+ font-family: var(--tai-font-sans);
395
+ font-size: var(--tai-text-sm);
396
+ line-height: 1.5;
397
+ color: var(--tai-color-text-muted);
398
+ }
399
+
400
+ /* Icon beside a message nobody here writes — a validation string, a schema
401
+ path, a browser exception. It therefore WRAPS and breaks inside a long token
402
+ rather than pushing the document sideways at 320 px, and the icon aligns to
403
+ the first line instead of floating in the middle of a wrapped block. */
404
+ .tai-field-error {
405
+ display: flex;
406
+ align-items: flex-start;
407
+ flex-wrap: wrap;
408
+ gap: var(--tai-space-2);
409
+ min-width: 0;
410
+ overflow-wrap: anywhere;
411
+ font-family: var(--tai-font-sans);
412
+ font-size: var(--tai-text-sm);
413
+ line-height: 1.5;
414
+ color: var(--tai-color-err-text);
415
+ }
416
+
417
+ .tai-input,
418
+ .tai-textarea,
419
+ .tai-select-trigger {
420
+ box-sizing: border-box;
421
+ width: 100%;
422
+ background: var(--tai-color-surface-raised);
423
+ color: var(--tai-color-text);
424
+ border: 1px solid var(--tai-color-control-border);
425
+ border-radius: var(--tai-radius-md);
426
+ font-family: var(--tai-font-sans);
427
+ font-size: var(--tai-text-md);
428
+ transition:
429
+ border-color var(--tai-motion-fast) ease,
430
+ background-color var(--tai-motion-fast) ease;
431
+ }
432
+
433
+ .tai-input,
434
+ .tai-select-trigger {
435
+ /* A FLOOR, not a fixed box — the same shape `.tai-btn` uses. The text inside
436
+ is authored in rem so a viewer's font-size preference scales it; a fixed
437
+ 36 px box with 8 px block padding and a 1px border leaves that text 18 px
438
+ to live in, so at a large root size a field would clip while the button
439
+ beside it grew. */
440
+ min-height: var(--tai-control-height);
441
+ /* The 8 × 16 control padding a button wears, so a field and the button
442
+ beside it start their text on the same inset. */
443
+ padding: var(--tai-space-2) var(--tai-space-4);
444
+ /* The button's line box, for the same reason: the trigger's chosen value is
445
+ caller data and breaks anywhere, and a line box shorter than the font's
446
+ content area overlaps the line under it. */
447
+ line-height: 1.25;
448
+ }
449
+
450
+ /* The trigger lays out a label beside its chevron; a text input has no
451
+ children to lay out, so the flex box belongs here alone. */
452
+ .tai-select-trigger {
453
+ display: flex;
454
+ align-items: center;
455
+ gap: var(--tai-space-2);
456
+ justify-content: space-between;
457
+ text-align: left;
458
+ cursor: pointer;
459
+ /* `width: 100%` sizes the BOX; it says nothing about what is inside it. The
460
+ `<Select.Value>` span still reports its own min-content, and the chosen
461
+ value is caller data — a server id, a plugin tag — so an unbroken one
462
+ pushes the DOCUMENT sideways out of the field it sits in. Measured
463
+ headless: a 35 / 41 / 44-character value pushed a document scrollbar at
464
+ 320 / 360 / 390 px, and with these two declarations no length does. The
465
+ span is a flex item of this row, so it needs the same pair every other
466
+ caller-sized control carries. */
467
+ min-width: 0;
468
+ overflow-wrap: anywhere;
469
+ }
470
+
471
+ .tai-textarea {
472
+ display: block;
473
+ min-height: calc(var(--tai-control-height) * 2);
474
+ padding: var(--tai-space-2) var(--tai-space-3);
475
+ line-height: 1.5;
476
+ resize: vertical;
477
+ }
478
+
479
+ .tai-input::placeholder,
480
+ .tai-textarea::placeholder {
481
+ color: var(--tai-color-placeholder);
482
+ opacity: 1;
483
+ }
484
+
485
+ /* Hover excludes the invalid state rather than sitting above it: `:not()`
486
+ carries its argument's specificity, so an unscoped hover rule would outrank
487
+ the attribute rule below and drop the error border on pointer-over.
488
+
489
+ It excludes the FOCUSED state for the same arithmetic and the opposite
490
+ reason. That `:not()` escalation put hover at (0,4,0) against
491
+ `:focus-visible`'s (0,2,0), so resting the pointer on the field the user is
492
+ typing in reverted the accent partner-border — the indicator declared
493
+ above as "the focus indicator's partner" — to the plain text colour. */
494
+ .tai-input:not([aria-invalid='true']):hover:not(:disabled):not(:focus-visible),
495
+ .tai-textarea:not([aria-invalid='true']):hover:not(:disabled):not(:focus-visible),
496
+ .tai-select-trigger:not([aria-invalid='true']):hover:not(:disabled):not(:focus-visible) {
497
+ border-color: var(--tai-color-text);
498
+ }
499
+
500
+ .tai-input:disabled,
501
+ .tai-textarea:disabled,
502
+ .tai-select-trigger:disabled {
503
+ background: var(--tai-color-surface-disabled);
504
+ color: var(--tai-color-text-disabled);
505
+ border-color: var(--tai-color-border-disabled);
506
+ cursor: not-allowed;
507
+ }
508
+
509
+ /* An invalid field is announced by its message and icon; the border is a
510
+ supporting cue, never the only one.
511
+
512
+ DISABLED WINS over invalid, and the exclusion is written rather than left to
513
+ source order: a control the operator cannot act on has to READ as one, and
514
+ the message and icon carry the invalidity either way. That is the same
515
+ ordering `.tai-checkbox:disabled` takes over its own checked ground, for the
516
+ same reason. Without the `:not()` these two rules are both (0,2,0) and the
517
+ later one simply wins, which put an error boundary on a field nobody can
518
+ correct. */
519
+ .tai-input[aria-invalid='true']:not(:disabled),
520
+ .tai-textarea[aria-invalid='true']:not(:disabled),
521
+ .tai-select-trigger[aria-invalid='true']:not(:disabled) {
522
+ border-color: var(--tai-color-err-text);
523
+ }
524
+
525
+ .tai-input-mono,
526
+ .tai-textarea-mono {
527
+ font-family: var(--tai-font-mono);
528
+ font-size: var(--tai-text-code);
529
+ }
530
+
531
+ /* The listbox is portalled to the document body, so it is a SIBLING of any
532
+ open dialog rather than a descendant — hence the popover layer, which
533
+ clears the dialog scrim that would otherwise cover and swallow it. The
534
+ height cap yields to the space the popper actually has, so a long list in a
535
+ short viewport still scrolls inside the screen; the fallback covers the
536
+ in-flow suggestion list, which wears these classes without a popper to
537
+ publish that measurement. */
538
+ .tai-select-content {
539
+ z-index: var(--tai-z-popover);
540
+ /* Capped against the VIEWPORT as well as its own content, like every other
541
+ floating surface: the width is set by the longest option label, which is
542
+ caller data and can exceed a 320 px screen on its own. */
543
+ max-width: min(20rem, calc(100vw - 2 * var(--tai-space-4)));
544
+ max-height: min(18rem, var(--radix-select-content-available-height, 60vh));
545
+ overflow-y: auto;
546
+ /* The cap is on the BORDER box, or it is not a cap at all: with the default
547
+ `content-box` the side padding and the 1 px boundary sit outside it. Radix
548
+ inline-styles `box-sizing: border-box` on its own popper, but the in-flow
549
+ suggestion list wears this class as a plain `<ul>` with no Radix near it. */
550
+ box-sizing: border-box;
551
+ background: var(--tai-color-surface-raised);
552
+ /* The popover's fill IS the page ground in light (`surface-raised` and `bg`
553
+ are both #ffffff, 1.000:1), so this edge is the whole of what says where
554
+ the surface begins — and a boundary that identifies a component is held to
555
+ 3:1 (WCAG 1.4.11). The decorative tier gives 1.47:1; the control tier gives
556
+ 4.21:1 light / 3.65:1 dark. */
557
+ border: 1px solid var(--tai-color-control-border);
558
+ border-radius: var(--tai-radius-md);
559
+ box-shadow: var(--tai-shadow-overlay);
560
+ }
561
+
562
+ /* The padding lives on the OVERFLOW box, not on its parent. Radix inline-styles
563
+ the viewport `overflow: hidden auto`, and the shared focus rule paints
564
+ `outline: 2px solid …; outline-offset: 2px` — 4 px outside the option's
565
+ border box. With the padding one level up, the viewport clipped the left and
566
+ right segments of every option's ring and the top or bottom segment of the
567
+ first and last, so a keyboard user saw two horizontal bars instead of a
568
+ ring. */
569
+ .tai-select-viewport {
570
+ /* 8 px against a 4 px ring reach (a 2 px outline at a 2 px offset), so the
571
+ gap is the ring PLUS a spacing step rather than exactly the ring. At 4 px
572
+ the two numbers are equal and every one of them — the outline width, the
573
+ offset, a scrollbar the viewport grows when the list is long — clips the
574
+ ring the moment it moves by a pixel, with no gate able to see it. The gate
575
+ reads both numbers off this sheet and holds the padding above the reach. */
576
+ padding: var(--tai-space-2);
577
+ /* Radix moves the keyboard highlight with `scrollIntoView({block:'nearest'})`,
578
+ which aligns to the PADDING box — so from the eighth option on, the scroll
579
+ consumes exactly what the padding above reserves and every step loses its
580
+ leading ring edge. `scroll-padding` keeps that gap on the scroll port too.
581
+ Measured over a 52-step keyboard walk: 36 steps clipped without it, 0 with
582
+ it. It matches the padding, so the two cannot drift apart. */
583
+ scroll-padding: var(--tai-space-2);
584
+ }
585
+
586
+ .tai-select-item {
587
+ display: flex;
588
+ align-items: center;
589
+ /* The option text sits left; the chosen-value mark sits at the far edge. */
590
+ justify-content: space-between;
591
+ gap: var(--tai-space-2);
592
+ padding: var(--tai-space-2) var(--tai-space-3);
593
+ border-radius: var(--tai-radius-sm);
594
+ font-family: var(--tai-font-sans);
595
+ font-size: var(--tai-text-md);
596
+ line-height: 1.4;
597
+ color: var(--tai-color-text);
598
+ cursor: pointer;
599
+ }
600
+
601
+ /* A group name is supplied by the caller — a plugin's own tag, not a string
602
+ this design system authored — so it keeps the label geometry but not the
603
+ uppercasing, which would rewrite a case-carrying name (`gRPC`, `iOS`). */
604
+ .tai-select-group-label {
605
+ padding: var(--tai-space-2) var(--tai-space-3) var(--tai-space-1);
606
+ text-transform: none;
607
+ }
608
+
609
+ /* Radix moves DOM focus to the highlighted Select.Item during keyboard and
610
+ pointer navigation, which stamps `data-highlighted`. The completion list
611
+ wears these same classes on its own buttons, which Radix never touches, so
612
+ pointer hover is named alongside it — the accent tint reads as the current
613
+ option in both themes (WCAG 2.4.7). */
614
+ .tai-select-item:not([data-disabled]):hover,
615
+ .tai-select-item[data-highlighted] {
616
+ background: var(--tai-color-accent-tint);
617
+ color: var(--tai-color-accent-on-tint);
618
+ }
619
+
620
+ /* Radix stamps `data-highlighted` for pointer hover as well as for keyboard
621
+ movement, and it moves DOM focus either way — so the highlight alone cannot
622
+ say which one happened. `:focus-visible` can, and the shared ring above is
623
+ already keyed on it, so the only state left to suppress is the UA ring a
624
+ browser may still draw on plain `:focus`. Guarding this on
625
+ `:not(:focus-visible)` is what keeps the keyboard ring: without it this rule
626
+ matches the keyboard case too, at equal specificity and later in the sheet,
627
+ and every Select loses its visible focus indicator (WCAG 2.4.7). */
628
+ .tai-select-item[data-highlighted]:not(:focus-visible) {
629
+ outline: none;
630
+ }
631
+
632
+ /* The highlight is only WHERE the reader is; it moves as they arrow through
633
+ the list. The chosen value needs a mark of its own, or it becomes
634
+ unidentifiable the moment any other option is highlighted. The mark is a
635
+ glyph rather than a color so it survives the highlight's own repaint, and it
636
+ renders only for the chosen option, so nothing shifts as the value moves. */
637
+ .tai-select-item-indicator {
638
+ display: flex;
639
+ flex: none;
640
+ }
641
+
642
+ .tai-select-item[data-disabled] {
643
+ color: var(--tai-color-text-disabled);
644
+ cursor: not-allowed;
645
+ }
646
+
647
+ /* -- Checkbox + radio ------------------------------------------------------ */
648
+ .tai-checkbox,
649
+ .tai-radio {
650
+ position: relative;
651
+ display: inline-flex;
652
+ align-items: center;
653
+ justify-content: center;
654
+ flex: none;
655
+ width: 1rem;
656
+ height: 1rem;
657
+ padding: 0;
658
+ background: var(--tai-color-surface-raised);
659
+ border: 1px solid var(--tai-color-control-border);
660
+ color: var(--tai-color-on-accent);
661
+ cursor: pointer;
662
+ }
663
+
664
+ /* 16 px is the VISUAL size of the box. The pointer target is grown past it by
665
+ a transparent overlay so a box with no adjacent label — a table's selection
666
+ column — still clears the 24 px minimum (WCAG 2.5.8) without the row growing
667
+ around it. */
668
+ .tai-checkbox::after,
669
+ .tai-radio::after {
670
+ content: '';
671
+ position: absolute;
672
+ inset: -4px;
673
+ }
674
+
675
+ .tai-checkbox {
676
+ border-radius: var(--tai-radius-sm);
677
+ }
678
+
679
+ .tai-radio {
680
+ border-radius: var(--tai-radius-full);
681
+ }
682
+
683
+ .tai-checkbox[data-state='checked'],
684
+ .tai-checkbox[data-state='indeterminate'] {
685
+ background: var(--tai-color-accent);
686
+ border-color: var(--tai-color-accent);
687
+ }
688
+
689
+ /* HOVER. The box strengthens its boundary to the text ink — the same move
690
+ `.tai-input` makes, and the reason both are legible over the raised ground
691
+ they sit on (17.7:1 light, 13.9:1 dark). A `.tai-choice` label is the box's
692
+ larger hit target, so hovering the label is hovering the control and moves
693
+ the box with it; the label's own ink is already the strongest body ink there
694
+ is, so there is nothing above it for the label itself to move to. */
695
+ .tai-checkbox:hover:not(:disabled),
696
+ .tai-radio:hover:not(:disabled),
697
+ .tai-choice:hover > .tai-checkbox:not(:disabled),
698
+ .tai-choice:hover > .tai-radio:not(:disabled) {
699
+ border-color: var(--tai-color-text);
700
+ }
701
+
702
+ /* A checked box keeps its accent GROUND and moves to the hover member of the
703
+ ramp instead. `:not()` carries its argument's specificity, so the rule above
704
+ sits at (0,3,0) / (0,4,0) and would otherwise repaint the checked boundary
705
+ the plain text colour while the fill stayed accent; these selectors match
706
+ its arithmetic and come after it. */
707
+ .tai-checkbox[data-state='checked']:hover:not(:disabled),
708
+ .tai-checkbox[data-state='indeterminate']:hover:not(:disabled),
709
+ .tai-choice:hover > .tai-checkbox[data-state='checked']:not(:disabled),
710
+ .tai-choice:hover > .tai-checkbox[data-state='indeterminate']:not(:disabled) {
711
+ background: var(--tai-color-accent-hover);
712
+ border-color: var(--tai-color-accent-hover);
713
+ }
714
+
715
+ .tai-radio-indicator {
716
+ display: block;
717
+ width: 0.5rem;
718
+ height: 0.5rem;
719
+ border-radius: var(--tai-radius-full);
720
+ background: var(--tai-color-accent);
721
+ }
722
+
723
+ /* The tick is drawn in `currentColor`, so the disabled ground has to bring its
724
+ own foreground with it: repainting the box without one left a checked AND
725
+ disabled control showing white on white. A read-only pane is exactly where
726
+ the checked state must still READ, so this is the same disabled foreground
727
+ every other control uses rather than a hidden one. `.tai-radio-indicator`
728
+ paints its own background and is unaffected. */
729
+ .tai-checkbox:disabled,
730
+ .tai-radio:disabled {
731
+ background: var(--tai-color-surface-disabled);
732
+ color: var(--tai-color-text-disabled);
733
+ border-color: var(--tai-color-border-disabled);
734
+ cursor: not-allowed;
735
+ }
736
+
737
+ .tai-choice {
738
+ display: inline-flex;
739
+ align-items: center;
740
+ gap: var(--tai-space-2);
741
+ min-height: var(--tai-control-height);
742
+ font-family: var(--tai-font-sans);
743
+ font-size: var(--tai-text-md);
744
+ line-height: 1.4;
745
+ color: var(--tai-color-text);
746
+ cursor: pointer;
747
+ /* A flex item's default `min-width: auto` refuses to shrink below its own
748
+ content, so caller text — a server label, a tag, an operator's own
749
+ string — pushes the document instead of breaking inside the control.
750
+ Measured at 320 px: without this the rule pushes a document scrollbar.
751
+ `flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
752
+ because the overflow is the control's own text, not its item order. */
753
+ min-width: 0;
754
+ overflow-wrap: anywhere;
755
+ }
756
+
757
+ /* -- Segmented control -----------------------------------------------------
758
+ A compact radiogroup whose options ARE their content: no dot indicator, an
759
+ icon plus a visually-hidden label. Vertical is the rail's layout, where a
760
+ horizontal trio cannot fit in 72 px. */
761
+ .tai-segmented {
762
+ display: inline-flex;
763
+ flex-wrap: wrap;
764
+ gap: var(--tai-space-1);
765
+ padding: var(--tai-space-1);
766
+ background: var(--tai-color-surface);
767
+ border: 1px solid var(--tai-color-border);
768
+ border-radius: var(--tai-radius-md);
769
+ }
770
+
771
+ .tai-segmented[data-orientation='vertical'] {
772
+ flex-direction: column;
773
+ }
774
+
775
+ .tai-segment {
776
+ display: inline-flex;
777
+ align-items: center;
778
+ justify-content: center;
779
+ gap: var(--tai-space-2);
780
+ box-sizing: border-box;
781
+ /* A hit-target FLOOR (WCAG 2.5.8), not a content floor: 28 px is a small
782
+ fraction of the narrowest viewport, so the option can still shrink far
783
+ below its own label — which is the shape the 320 px contract asks for,
784
+ and the reason this rule keeps its `min-width` rather than dropping to
785
+ `0`. What the label needs on top of it is permission to BREAK.
786
+ `visuallyHiddenLabel` is opt-in PER OPTION, so the API default paints the
787
+ caller's own string beside the icon; measured headless, an unbroken one
788
+ pushed a document scrollbar from 43 / 48 / 53 characters at
789
+ 320 / 360 / 390 px, and with `overflow-wrap` no length does. */
790
+ min-width: 28px;
791
+ min-height: 28px;
792
+ overflow-wrap: anywhere;
793
+ padding: var(--tai-space-1) var(--tai-space-2);
794
+ border: 1px solid transparent;
795
+ border-radius: var(--tai-radius-sm);
796
+ background: transparent;
797
+ color: var(--tai-color-text-muted);
798
+ font-family: var(--tai-font-sans);
799
+ font-size: var(--tai-text-sm);
800
+ font-weight: 500;
801
+ line-height: 1.25;
802
+ cursor: pointer;
803
+ transition:
804
+ background-color var(--tai-motion-fast) ease,
805
+ color var(--tai-motion-fast) ease;
806
+ }
807
+
808
+ .tai-segment:not([data-state='checked']):hover:not(:disabled) {
809
+ background: var(--tai-color-surface-raised);
810
+ color: var(--tai-color-text);
811
+ }
812
+
813
+ .tai-segment[data-state='checked'] {
814
+ background: var(--tai-color-accent-tint);
815
+ border-color: var(--tai-color-accent);
816
+ color: var(--tai-color-accent-on-tint);
817
+ }
818
+
819
+ /* The selected segment KEEPS its tint on hover — it is the only cue an
820
+ icon-only option has — and strengthens its border instead. The border is the
821
+ part that moves in BOTH themes: in dark the accent-hover ink is the same
822
+ value as accent-on-tint, so a color change there would be invisible. */
823
+ .tai-segment[data-state='checked']:hover:not(:disabled) {
824
+ border-color: var(--tai-color-accent-hover);
825
+ }
826
+
827
+ .tai-segment:disabled {
828
+ color: var(--tai-color-text-disabled);
829
+ cursor: not-allowed;
830
+ }
831
+
832
+ /* -- Cards ----------------------------------------------------------------- */
833
+ .tai-card {
834
+ box-sizing: border-box;
835
+ background: var(--tai-color-surface-raised);
836
+ border: 1px solid var(--tai-color-border);
837
+ border-radius: var(--tai-radius-lg);
838
+ padding: var(--tai-space-5);
839
+ color: var(--tai-color-text);
840
+ transition:
841
+ border-color var(--tai-motion-fast) ease,
842
+ box-shadow var(--tai-motion-base) ease,
843
+ transform var(--tai-motion-base) ease;
844
+ }
845
+
846
+ .tai-card-interactive:hover,
847
+ .tai-card-interactive:focus-within {
848
+ transform: translateY(-4px);
849
+ border-color: var(--tai-color-border-strong);
850
+ box-shadow: var(--tai-shadow-lift);
851
+ }
852
+
853
+ .tai-card-title {
854
+ margin: 0;
855
+ font-size: var(--tai-text-lg);
856
+ font-weight: 600;
857
+ line-height: 1.3;
858
+ color: var(--tai-color-heading);
859
+ letter-spacing: -0.03em;
860
+ }
861
+
862
+ /* -- Tabs ------------------------------------------------------------------ */
863
+ .tai-tablist {
864
+ display: flex;
865
+ /* Tab labels are caller data and a plugin adds its own, so the strip wraps
866
+ rather than pushing the DOCUMENT sideways — the five Settings tabs alone
867
+ need 394 px and there is no horizontal scroll to reach them at 320. Every
868
+ strip in this sheet that lays CALLER-SIZED content out in a row wraps for
869
+ the same reason; `narrow-viewport.test.ts` reconciles that set against the
870
+ sheet in both directions, so a new one cannot be silently exempted. */
871
+ flex-wrap: wrap;
872
+ gap: var(--tai-space-1);
873
+ border-bottom: 1px solid var(--tai-color-border);
874
+ }
875
+
876
+ .tai-tab {
877
+ appearance: none;
878
+ box-sizing: border-box;
879
+ min-height: var(--tai-control-height);
880
+ padding: var(--tai-space-2) var(--tai-space-3);
881
+ background: transparent;
882
+ border: none;
883
+ border-bottom: 2px solid transparent;
884
+ font-family: var(--tai-font-sans);
885
+ font-size: var(--tai-text-md);
886
+ font-weight: 500;
887
+ line-height: 1.25;
888
+ color: var(--tai-color-text-muted);
889
+ cursor: pointer;
890
+ transition:
891
+ color var(--tai-motion-fast) ease,
892
+ border-color var(--tai-motion-fast) ease;
893
+ }
894
+
895
+ .tai-tab:hover:not(:disabled) {
896
+ color: var(--tai-color-text);
897
+ }
898
+
899
+ .tai-tab[data-state='active'] {
900
+ color: var(--tai-color-text);
901
+ border-bottom-color: var(--tai-color-accent);
902
+ font-weight: 600;
903
+ }
904
+
905
+ .tai-tab:disabled {
906
+ color: var(--tai-color-text-disabled);
907
+ cursor: not-allowed;
908
+ }
909
+
910
+ .tai-tabpanel {
911
+ padding: var(--tai-space-4) 0;
912
+ color: var(--tai-color-text);
913
+ }
914
+
915
+ /* -- Navigation ------------------------------------------------------------
916
+ The current item carries three cues at once: an accent tint, accent-on-tint
917
+ text, and a 3 px inset rail — plus `aria-current="page"` for assistive tech.
918
+ `.tai-nav-link` is the shell's existing name for the same item. */
919
+ .tai-nav-item,
920
+ .tai-nav-link {
921
+ display: flex;
922
+ align-items: center;
923
+ gap: var(--tai-space-2);
924
+ box-sizing: border-box;
925
+ /* A flex item's default `min-width: auto` refuses to shrink below its own
926
+ content, so caller text — a server label, a tag, an operator's own
927
+ string — pushes the document instead of breaking inside the control.
928
+ Measured at 320 px: without this the rule pushes a document scrollbar.
929
+ `flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
930
+ because the overflow is the control's own text, not its item order. */
931
+ min-width: 0;
932
+ overflow-wrap: anywhere;
933
+ min-height: var(--tai-control-height);
934
+ padding: var(--tai-space-2) var(--tai-space-3);
935
+ border-radius: var(--tai-radius-md);
936
+ font-family: var(--tai-font-sans);
937
+ font-size: var(--tai-text-md);
938
+ font-weight: 500;
939
+ line-height: 1.25;
940
+ color: var(--tai-color-text);
941
+ text-decoration: none;
942
+ transition:
943
+ background-color var(--tai-motion-fast) ease,
944
+ color var(--tai-motion-fast) ease;
945
+ }
946
+
947
+ /* A nav item lives in CHROME, not on the page ground, and chrome is painted
948
+ from `--tai-color-surface` — `.tai-shell-sidebar` below is, and so is any
949
+ host sidebar that follows it. Stepping the hover to that same tone therefore
950
+ resolved to 1.000:1 against the ground the item was sitting on: hovering a
951
+ nav item changed nothing at all. The line tone is the one near-ground token
952
+ that is distinct from every ground this sheet paints, derived statically
953
+ from the token hex values: against `surface` 1.41:1 light / 1.78:1 dark,
954
+ against `bg` 1.47:1 / 1.88:1, against `surface-raised` 1.47:1 / 1.66:1. The
955
+ item's own ink stays well clear of it (12.0:1 light, 8.4:1 dark). */
956
+ .tai-nav-item:hover,
957
+ .tai-nav-link:hover {
958
+ background: var(--tai-color-border-strong);
959
+ }
960
+
961
+ .tai-nav-item[aria-current='page'],
962
+ .tai-nav-link[aria-current='page'] {
963
+ background: var(--tai-color-accent-tint);
964
+ color: var(--tai-color-accent-on-tint);
965
+ font-weight: 600;
966
+ box-shadow: inset 3px 0 0 0 var(--tai-color-accent);
967
+ }
968
+
969
+ /* The current row answers the pointer too. `[aria-current='page']` and `:hover`
970
+ are both (0,2,0), so the rule above wins by ORDER and the one row a reader is
971
+ standing on is the only row in the rail that does not respond.
972
+
973
+ It cannot take the line tone the other rows take. The tint is translucent, so
974
+ that tone shows through it and carries `accent-on-tint` down to 3.79:1 light /
975
+ 3.33:1 dark, under the 4.5:1 the label itself needs (WCAG 1.4.3) — the ground
976
+ step is bounded by the ink sitting on it. The tint is DOUBLED instead: a
977
+ second layer of the same token over the first, which deepens the ground to
978
+ 1.13:1 light / 1.16:1 dark against the resting row while the label still
979
+ clears its floor at 4.67:1 / 5.05:1. All four figures are derived statically
980
+ from the token values. */
981
+ .tai-nav-item[aria-current='page']:hover,
982
+ .tai-nav-link[aria-current='page']:hover {
983
+ background-image: linear-gradient(var(--tai-color-accent-tint), var(--tai-color-accent-tint));
984
+ }
985
+
986
+ /* Skip-to-content: off-screen until focused, so a keyboard or screen-reader
987
+ user can bypass the sidebar nav and jump straight to the page content
988
+ (WCAG 2.4.1). It is the first focusable element in the shell. */
989
+ .tai-skip-link {
990
+ position: absolute;
991
+ left: var(--tai-space-2);
992
+ top: -4rem;
993
+ z-index: var(--tai-z-tooltip);
994
+ padding: var(--tai-space-2) var(--tai-space-3);
995
+ border-radius: var(--tai-radius-md);
996
+ background: var(--tai-color-accent);
997
+ color: var(--tai-color-on-accent);
998
+ font-family: var(--tai-font-sans);
999
+ font-size: var(--tai-text-md);
1000
+ line-height: 1.4;
1001
+ text-decoration: none;
1002
+ transition: top var(--tai-motion-fast) ease-in;
1003
+ }
1004
+
1005
+ .tai-skip-link:focus {
1006
+ top: var(--tai-space-2);
1007
+ }
1008
+
1009
+ /* -- Chips ------------------------------------------------------------------
1010
+ `.tai-chip` publishes a CONTROL: a pointer, a hover, a pressed ground, a
1011
+ disabled rendering and a 44 px coarse target, for a consumer's filter or
1012
+ toggle chip. Every chip this repo renders is a TAG and opts out of all of it
1013
+ with `.tai-chip-static`, so the interactive half is reachable only from
1014
+ outside — which is a published-surface affordance, not dead code, and
1015
+ `class-contract.test.ts` records it as one by name. */
1016
+ .tai-chip {
1017
+ display: inline-flex;
1018
+ align-items: center;
1019
+ gap: var(--tai-space-1);
1020
+ box-sizing: border-box;
1021
+ min-height: 24px;
1022
+ padding: var(--tai-space-1) var(--tai-space-3);
1023
+ border: 1px solid var(--tai-color-control-border);
1024
+ border-radius: var(--tai-radius-full);
1025
+ background: var(--tai-color-surface-raised);
1026
+ color: var(--tai-color-text);
1027
+ font-family: var(--tai-font-sans);
1028
+ font-size: var(--tai-text-sm);
1029
+ line-height: 1.25;
1030
+ cursor: pointer;
1031
+ /* A flex item's default `min-width: auto` refuses to shrink below its own
1032
+ content, so caller text — a server label, a tag, an operator's own
1033
+ string — pushes the document instead of breaking inside the control.
1034
+ Measured at 320 px: without this the rule pushes a document scrollbar.
1035
+ `flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
1036
+ because the overflow is the control's own text, not its item order. */
1037
+ min-width: 0;
1038
+ overflow-wrap: anywhere;
1039
+ transition:
1040
+ background-color var(--tai-motion-fast) ease,
1041
+ border-color var(--tai-motion-fast) ease,
1042
+ color var(--tai-motion-fast) ease;
1043
+ }
1044
+
1045
+ .tai-chip:not(.tai-chip-static):not([aria-pressed='true']):hover:not(:disabled) {
1046
+ background: var(--tai-color-surface);
1047
+ }
1048
+
1049
+ .tai-chip[aria-pressed='true'] {
1050
+ background: var(--tai-color-accent-tint);
1051
+ border-color: var(--tai-color-accent);
1052
+ color: var(--tai-color-accent-on-tint);
1053
+ }
1054
+
1055
+ /* A selected chip KEEPS its tint on hover and strengthens its border instead —
1056
+ the one part that moves in both themes (see the segment rule above). */
1057
+ .tai-chip[aria-pressed='true']:hover:not(:disabled) {
1058
+ border-color: var(--tai-color-accent-hover);
1059
+ }
1060
+
1061
+ .tai-chip:disabled {
1062
+ color: var(--tai-color-text-disabled);
1063
+ border-color: var(--tai-color-border-disabled);
1064
+ cursor: not-allowed;
1065
+ }
1066
+
1067
+ /* A chip that only reads (a tag, a "+N more" count) is not a control, so it
1068
+ shows no pointer and is excluded from the hover rule above. */
1069
+ .tai-chip-static {
1070
+ cursor: default;
1071
+ }
1072
+
1073
+ /* -- Badges -----------------------------------------------------------------
1074
+ A badge is tint + its own `-text` color. The solid variants are for chart
1075
+ legends and status fills, and always carry the ink label. */
1076
+ .tai-badge {
1077
+ display: inline-flex;
1078
+ align-items: center;
1079
+ gap: var(--tai-space-1);
1080
+ box-sizing: border-box;
1081
+ min-height: 20px;
1082
+ padding: var(--tai-space-1) var(--tai-space-2);
1083
+ border-radius: var(--tai-radius-full);
1084
+ background: var(--tai-color-accent-tint);
1085
+ color: var(--tai-color-accent-on-tint);
1086
+ font-family: var(--tai-font-sans);
1087
+ font-size: var(--tai-text-xs);
1088
+ font-weight: 500;
1089
+ line-height: 1.4;
1090
+ /* A flex item's default `min-width: auto` refuses to shrink below its own
1091
+ content, so caller text — a server label, a tag, an operator's own
1092
+ string — pushes the document instead of breaking inside the control.
1093
+ Measured at 320 px: without this the rule pushes a document scrollbar.
1094
+ `flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
1095
+ because the overflow is the control's own text, not its item order. */
1096
+ min-width: 0;
1097
+ overflow-wrap: anywhere;
1098
+ }
1099
+
1100
+ .tai-badge-neutral {
1101
+ background: var(--tai-color-surface);
1102
+ color: var(--tai-color-text-muted);
1103
+ }
1104
+
1105
+ .tai-badge-ok {
1106
+ background: var(--tai-color-ok-tint);
1107
+ color: var(--tai-color-ok-text);
1108
+ }
1109
+
1110
+ .tai-badge-err {
1111
+ background: var(--tai-color-err-tint);
1112
+ color: var(--tai-color-err-text);
1113
+ }
1114
+
1115
+ .tai-badge-warn {
1116
+ background: var(--tai-color-warn-tint);
1117
+ color: var(--tai-color-warn-text);
1118
+ }
1119
+
1120
+ .tai-badge-solid-ok {
1121
+ background: var(--tai-color-ok-fill);
1122
+ color: var(--tai-color-on-fill);
1123
+ }
1124
+
1125
+ .tai-badge-solid-err {
1126
+ background: var(--tai-color-err-fill);
1127
+ color: var(--tai-color-on-fill);
1128
+ }
1129
+
1130
+ .tai-badge-solid-warn {
1131
+ background: var(--tai-color-warn-fill);
1132
+ color: var(--tai-color-on-fill);
1133
+ }
1134
+
1135
+ /* -- Status ----------------------------------------------------------------
1136
+ Status is always an icon plus a label; the color only reinforces it. */
1137
+ .tai-status {
1138
+ display: inline-flex;
1139
+ /* `flex-start`, not `center`: once the label wraps, a centred mark floats
1140
+ between the two lines instead of sitting on the first one — the exact
1141
+ reason `.tai-field-error` gives for its own `flex-start`. Measured at
1142
+ 320/360/375/390 px: icon offset 11 px -> 0, box height unchanged.
1143
+ It moves the SINGLE-line case too, by design and not only as a side
1144
+ effect: a 13.5 px label on a 1.4 line box is an 18.9 px line, so a 16 px
1145
+ mark centred in it sat 1.45 px below the line's top edge and now sits on
1146
+ it. That is the alignment the wrapped case needs, and one line of text is
1147
+ the wrapped case with one line. */
1148
+ align-items: flex-start;
1149
+ gap: var(--tai-space-2);
1150
+ font-family: var(--tai-font-sans);
1151
+ font-size: var(--tai-text-md);
1152
+ line-height: 1.4;
1153
+ /* A flex item's default `min-width: auto` refuses to shrink below its own
1154
+ content, so caller text — a server label, a tag, an operator's own
1155
+ string — pushes the document instead of breaking inside the control.
1156
+ Measured at 320 px: without this the rule pushes a document scrollbar.
1157
+ `flex-wrap: wrap` does NOT substitute; it is a measured no-op here,
1158
+ because the overflow is the control's own text, not its item order. */
1159
+ min-width: 0;
1160
+ overflow-wrap: anywhere;
1161
+ }
1162
+
1163
+ .tai-status-ok {
1164
+ color: var(--tai-color-ok-text);
1165
+ }
1166
+
1167
+ .tai-status-err {
1168
+ color: var(--tai-color-err-text);
1169
+ }
1170
+
1171
+ .tai-status-warn {
1172
+ color: var(--tai-color-warn-text);
1173
+ }
1174
+
1175
+ .tai-status-pending {
1176
+ color: var(--tai-color-text-muted);
1177
+ }
1178
+
1179
+ /* -- Tables -----------------------------------------------------------------
1180
+ Mono uppercase headers, tinted row hover, numerics right-aligned on
1181
+ tabular figures so columns line up. */
1182
+ /* A table cell is not a flex item. Auto table layout distributes width from
1183
+ each column's MIN-CONTENT, and `overflow-wrap: anywhere` — unlike
1184
+ `break-word` — is taken into account when that min-content is computed. So a
1185
+ column holding one of these controls reported a min-content of ONE CHARACTER
1186
+ and the label was laid out a letter per line: measured, a live 5-column
1187
+ table rendered its "Revoke" button 6 lines / 99 px at 320-390 px and still 2
1188
+ lines / 45 px at 768 px, which is not a narrow viewport at all. Because
1189
+ `.tai-table` is `width: 100%`, this also fired INSIDE a `.tai-scroll-region`:
1190
+ the table stopped wanting to be wider than the box, so the region's
1191
+ `overflow-x: auto` never engaged and the control was mangled instead of the
1192
+ table scrolling. Inside a table these controls therefore keep their
1193
+ intrinsic minimum; the 320 px contract outside tables is untouched.
1194
+
1195
+ The group is EVERY caller-sized item, not only the ones a cell holds today.
1196
+ Live table call sites exist for four of them — `.tai-btn` in the actions
1197
+ column of seven screens, `.tai-badge` in eleven tables, `.tai-chip` via
1198
+ TagChips, `.tai-status` in VersionHistoryPanel. The rest are GUARD
1199
+ declarations, and this comment is not claiming a nav item or a select is
1200
+ rendered in a cell: they are not. That is deliberate rather than sloppy. A
1201
+ rule matching nothing costs one line here and nothing at runtime, whereas a
1202
+ hand-audited "these ones cannot appear in a table" list costs a live,
1203
+ user-visible regression on the day someone adds the call site — and this
1204
+ contract has now been bitten by exactly that twice. `narrow-viewport.test.ts`
1205
+ reconciles this group against its caller-sized list as a set equality in
1206
+ BOTH directions, so adding an item forces both rules or the gate reddens. */
1207
+ .tai-table .tai-btn,
1208
+ .tai-table .tai-badge,
1209
+ .tai-table .tai-chip,
1210
+ .tai-table .tai-status,
1211
+ .tai-table .tai-choice,
1212
+ .tai-table .tai-nav-item,
1213
+ .tai-table .tai-nav-link,
1214
+ .tai-table .tai-select-trigger {
1215
+ min-width: auto;
1216
+ overflow-wrap: normal;
1217
+ }
1218
+
1219
+ /* `.tai-segment` takes the wrap half ONLY. The eight above carry
1220
+ `min-width: 0`, so `auto` is what hands them an intrinsic minimum back;
1221
+ this one carries a 28 px / 44 px POINTER TARGET (WCAG 2.5.8), and `auto` on
1222
+ a flex item is the CONTENT-based minimum — for the icon-only option this
1223
+ control is designed around that is roughly 26 px, i.e. below the target. Its
1224
+ own floor already IS the non-zero minimum the rule above is restoring, so
1225
+ restoring it would only lower it. */
1226
+ .tai-table .tai-segment {
1227
+ overflow-wrap: normal;
1228
+ }
1229
+
1230
+ .tai-table {
1231
+ width: 100%;
1232
+ border-collapse: collapse;
1233
+ font-family: var(--tai-font-sans);
1234
+ font-size: var(--tai-text-md);
1235
+ line-height: 1.5;
1236
+ color: var(--tai-color-text);
1237
+ }
1238
+
1239
+ .tai-table th {
1240
+ padding: var(--tai-space-2) var(--tai-space-3);
1241
+ text-align: left;
1242
+ vertical-align: bottom;
1243
+ border-bottom: 1px solid var(--tai-color-border);
1244
+ }
1245
+
1246
+ .tai-table td {
1247
+ padding: var(--tai-space-3);
1248
+ vertical-align: top;
1249
+ border-bottom: 1px solid var(--tai-color-border);
1250
+ }
1251
+
1252
+ /* The row hover is a LOCATOR; a diff row's tint is MEANING, and meaning keeps
1253
+ the ground — the same call `.tai-segment[data-state='checked']:hover` makes
1254
+ for the one cue an icon-only option has. Written as an exclusion because the
1255
+ arithmetic goes the other way on its own: `.tai-table tbody tr:hover` is
1256
+ (0,1,2) and `.tai-diff-added` is (0,1,0), so every hovered diff row lost its
1257
+ colour to the accent tint. `:not()` carries its argument's specificity, so
1258
+ naming the three here is also what lifts this rule clear of them. The gate
1259
+ reconciles this list against the sheet's own semantic row grounds. */
1260
+ .tai-table tbody tr:not(.tai-diff-added):not(.tai-diff-removed):not(.tai-diff-changed):hover {
1261
+ background: var(--tai-color-accent-tint);
1262
+ }
1263
+
1264
+ .tai-table td[data-numeric='true'],
1265
+ .tai-table th[data-numeric='true'] {
1266
+ text-align: right;
1267
+ font-variant-numeric: tabular-nums;
1268
+ }
1269
+
1270
+ .tai-table-id {
1271
+ font-family: var(--tai-font-mono);
1272
+ font-size: var(--tai-text-code);
1273
+ }
1274
+
1275
+ /* -- Scroll regions ---------------------------------------------------------
1276
+ Horizontal overflow scrolls inside its own container. The tab stop, the
1277
+ `role="region"`, and the label are applied by `ScrollRegion` while the
1278
+ container overflows, or while it still holds focus — a table that fits and
1279
+ has never been focused is not a tab stop. */
1280
+ .tai-scroll-region {
1281
+ overflow-x: auto;
1282
+ max-width: 100%;
1283
+ }
1284
+
1285
+ /* -- Surfaces: dialog, drawer, tooltip --------------------------------------- */
1286
+ .tai-overlay {
1287
+ position: fixed;
1288
+ inset: 0;
1289
+ z-index: var(--tai-z-overlay);
1290
+ background: var(--tai-color-scrim);
1291
+ }
1292
+
1293
+ .tai-dialog {
1294
+ position: fixed;
1295
+ top: 50%;
1296
+ left: 50%;
1297
+ z-index: var(--tai-z-dialog);
1298
+ transform: translate(-50%, -50%);
1299
+ box-sizing: border-box;
1300
+ width: min(560px, calc(100vw - 2 * var(--tai-space-4)));
1301
+ /* `vh` is the LARGE viewport: on a phone with a retracting browser chrome the
1302
+ cap is taller than the screen by the height of that chrome, so a tall dialog
1303
+ runs its own actions row under the URL bar — and it is a MODAL, so there is
1304
+ no page behind it to scroll the bar away with. `dvh` is the live value. The
1305
+ pair is the fallback, not a preference: `dvh` arrived after this package's
1306
+ floor (Chrome 108, Firefox 101), and a browser below it drops the second
1307
+ declaration and keeps the first. */
1308
+ max-height: calc(100vh - 2 * var(--tai-space-6));
1309
+ max-height: calc(100dvh - 2 * var(--tai-space-6));
1310
+ overflow-y: auto;
1311
+ padding: var(--tai-space-6);
1312
+ background: var(--tai-color-surface-raised);
1313
+ border: 1px solid var(--tai-color-border);
1314
+ border-radius: var(--tai-radius-overlay);
1315
+ box-shadow: var(--tai-shadow-overlay);
1316
+ color: var(--tai-color-text);
1317
+ }
1318
+
1319
+ .tai-dialog-title {
1320
+ margin: 0 0 var(--tai-space-2);
1321
+ font-size: var(--tai-text-section);
1322
+ line-height: 1.25;
1323
+ }
1324
+
1325
+ .tai-dialog-actions {
1326
+ display: flex;
1327
+ /* `flex-end` sends any overflow LEFT, where LTR scrolling never reaches, and
1328
+ the confirm label is caller data. Wrapping keeps every action on screen. */
1329
+ flex-wrap: wrap;
1330
+ justify-content: flex-end;
1331
+ gap: var(--tai-space-2);
1332
+ margin-top: var(--tai-space-5);
1333
+ }
1334
+
1335
+ /* A side-anchored, full-height modal panel, published for a host to mount and
1336
+ drive itself. Radix Dialog supplies the trap, the scroll lock, Esc, the
1337
+ inert background, and focus return. */
1338
+ .tai-drawer {
1339
+ position: fixed;
1340
+ top: 0;
1341
+ bottom: 0;
1342
+ left: 0;
1343
+ z-index: var(--tai-z-dialog);
1344
+ box-sizing: border-box;
1345
+ display: flex;
1346
+ flex-direction: column;
1347
+ gap: var(--tai-space-3);
1348
+ width: min(280px, calc(100vw - var(--tai-space-8)));
1349
+ overflow-y: auto;
1350
+ padding: var(--tai-space-4);
1351
+ background: var(--tai-color-surface-raised);
1352
+ border-right: 1px solid var(--tai-color-border);
1353
+ box-shadow: var(--tai-shadow-overlay);
1354
+ color: var(--tai-color-text);
1355
+ }
1356
+
1357
+ .tai-drawer[data-side='right'] {
1358
+ left: auto;
1359
+ right: 0;
1360
+ border-right: none;
1361
+ border-left: 1px solid var(--tai-color-border);
1362
+ }
1363
+
1364
+ /* Caller-supplied title beside a close control: it wraps rather than pushing
1365
+ the drawer's own scroller sideways. */
1366
+ .tai-drawer-header {
1367
+ display: flex;
1368
+ align-items: center;
1369
+ justify-content: space-between;
1370
+ flex-wrap: wrap;
1371
+ gap: var(--tai-space-2);
1372
+ min-width: 0;
1373
+ }
1374
+
1375
+ .tai-tooltip {
1376
+ z-index: var(--tai-z-tooltip);
1377
+ /* The cap is on the BORDER box, or it is not a cap at all: with the default
1378
+ `content-box` the 12 px side padding and the 1 px boundary sit outside it,
1379
+ so at 320 px the 288 px cap painted a 314 px tip and pushed the document
1380
+ — for any consumer whose host has no preflight to set `box-sizing`
1381
+ globally. Every other capped surface in this sheet already says so. */
1382
+ box-sizing: border-box;
1383
+ max-width: min(20rem, calc(100vw - 2 * var(--tai-space-4)));
1384
+ padding: var(--tai-space-2) var(--tai-space-3);
1385
+ background: var(--tai-color-surface-raised);
1386
+ /* The tip's fill IS the page ground in light (`surface-raised` and `bg` are
1387
+ both #ffffff, 1.000:1) and it has no scrim under it, so this edge is the
1388
+ whole of what says where the tip begins — a boundary that identifies a
1389
+ component, held to 3:1 (WCAG 1.4.11). The decorative tier gives 1.47:1; the
1390
+ control tier gives 4.21:1 light / 3.65:1 dark. */
1391
+ border: 1px solid var(--tai-color-control-border);
1392
+ border-radius: var(--tai-radius-md);
1393
+ box-shadow: var(--tai-shadow-overlay);
1394
+ font-family: var(--tai-font-sans);
1395
+ font-size: var(--tai-text-sm);
1396
+ line-height: 1.5;
1397
+ color: var(--tai-color-text);
1398
+ }
1399
+
1400
+ /* -- Code, JSON, terminal surfaces -------------------------------------------- */
1401
+ .tai-code {
1402
+ padding: 0.1em 0.35em;
1403
+ background: var(--tai-color-code-bg);
1404
+ border-radius: var(--tai-radius-code);
1405
+ font-family: var(--tai-font-mono);
1406
+ font-size: var(--tai-text-code);
1407
+ color: var(--tai-color-text);
1408
+ }
1409
+
1410
+ /* ONE code surface, under two names. `.tai-prose pre` is the same block for
1411
+ markdown this Studio did not author, so it is the same rule: a copy of these
1412
+ declarations beside it had already drifted — a `tab-size` of 8 against 2
1413
+ rendered the identical listing at four times the indent, and without
1414
+ `box-sizing` the padding and boundary sat outside the block's own width. */
1415
+ .tai-code-block,
1416
+ .tai-prose pre {
1417
+ box-sizing: border-box;
1418
+ padding: var(--tai-space-4);
1419
+ overflow-x: auto;
1420
+ background: var(--tai-color-code-bg);
1421
+ border: 1px solid var(--tai-color-border);
1422
+ border-radius: var(--tai-radius-md);
1423
+ font-family: var(--tai-font-mono);
1424
+ font-size: var(--tai-text-code);
1425
+ line-height: 1.75;
1426
+ color: var(--tai-color-text);
1427
+ white-space: pre;
1428
+ tab-size: 2;
1429
+ }
1430
+
1431
+ /* The margin is the ONE declaration the two names do not share. A standalone
1432
+ block is placed by whatever stack it sits in, so it brings none of its own;
1433
+ a `<pre>` in prose is placed by `.tai-prose > * + *`, and a descendant
1434
+ selector outranks that rule, so zeroing it here would delete the prose
1435
+ rhythm above every code listing a README carries. */
1436
+ .tai-code-block {
1437
+ margin: 0;
1438
+ }
1439
+
1440
+ /* A code surface holding a message rather than a listing: it WRAPS instead of
1441
+ scrolling, so a long line stays readable without becoming a scroll container
1442
+ that a keyboard would have to reach. */
1443
+ .tai-code-block-wrap {
1444
+ overflow-x: visible;
1445
+ white-space: pre-wrap;
1446
+ overflow-wrap: anywhere;
1447
+ }
1448
+
1449
+ .tai-syntax-key {
1450
+ color: var(--tai-color-syntax-key);
1451
+ }
1452
+
1453
+ .tai-syntax-string {
1454
+ color: var(--tai-color-syntax-string);
1455
+ }
1456
+
1457
+ .tai-syntax-number {
1458
+ color: var(--tai-color-syntax-number);
1459
+ }
1460
+
1461
+ .tai-syntax-bool {
1462
+ color: var(--tai-color-syntax-bool);
1463
+ }
1464
+
1465
+ .tai-syntax-null {
1466
+ color: var(--tai-color-text-muted);
1467
+ }
1468
+
1469
+ .tai-diff-added {
1470
+ background: var(--tai-color-ok-tint);
1471
+ color: var(--tai-color-ok-text);
1472
+ }
1473
+
1474
+ .tai-diff-removed {
1475
+ background: var(--tai-color-err-tint);
1476
+ color: var(--tai-color-err-text);
1477
+ }
1478
+
1479
+ .tai-diff-changed {
1480
+ background: var(--tai-color-warn-tint);
1481
+ color: var(--tai-color-warn-text);
1482
+ }
1483
+
1484
+ /* -- Feedback surfaces --------------------------------------------------------- */
1485
+ /* The block rests at the LINE tone and sweeps the raised tone across it. That
1486
+ resting tone is also what the reduced-motion rule below leaves behind, so
1487
+ the moving and the parked skeleton are the same block — one with a band
1488
+ travelling over it. Basing it on `surface` instead put the whole block a
1489
+ step from the page ground (1.05:1 light, 1.07:1 dark — no visible band at
1490
+ all, and dimmer than the parked state it was meant to enrich); on the line
1491
+ tone the band reads at 1.24:1 light and 1.22:1 dark, and the block itself at
1492
+ 1.24:1 / 1.38:1 against the ground it sits on.
1493
+ `overflow: hidden` is what clips the band to the block's rounded corners. */
1494
+ .tai-skeleton {
1495
+ position: relative;
1496
+ overflow: hidden;
1497
+ border-radius: var(--tai-radius-sm);
1498
+ background: var(--tai-color-border);
1499
+ }
1500
+
1501
+ /* The band rides on an overlay rather than on the block's own
1502
+ `background-position`: a background-position animation restyles and repaints
1503
+ the block on the main thread every frame, for every skeleton on the page,
1504
+ while a transform on an absolutely-positioned overlay is composited off it.
1505
+ Both ends of the gradient are fully transparent, so what shows through at the
1506
+ band's edges is the block's own resting tone — the sweep adds a streak rather
1507
+ than fading the block into the page. */
1508
+ .tai-skeleton::after {
1509
+ content: '';
1510
+ position: absolute;
1511
+ inset: 0;
1512
+ background: linear-gradient(
1513
+ 90deg,
1514
+ transparent 0%,
1515
+ var(--tai-color-surface-raised) 50%,
1516
+ transparent 100%
1517
+ );
1518
+ animation: tai-shimmer 1.4s linear infinite;
1519
+ }
1520
+
1521
+ /* A busy indicator is a non-text UI component: its RING is the whole of what
1522
+ identifies it, so the ring is held to 3:1 (WCAG 1.4.11) and cannot sit in
1523
+ the decorative line tier. Derived statically from the token hex values, the
1524
+ decorative `border-strong` gave 1.47:1 light / 1.88:1 dark on the page
1525
+ ground and 1.47:1 / 1.66:1 on a raised one; the contrast-safe control tier
1526
+ gives 4.21:1 / 3.65:1 on the page ground, 4.21:1 / 3.23:1 raised, 4.03:1 /
1527
+ 3.45:1 on `surface` and 3.82:1 / 3.51:1 on the code ground. The leading arc
1528
+ is the accent, at 4.99:1 / 5.36:1 — the same argument `.tai-progress-track`
1529
+ makes for its own boundary. */
1530
+ .tai-spinner {
1531
+ display: inline-block;
1532
+ box-sizing: border-box;
1533
+ width: 16px;
1534
+ height: 16px;
1535
+ border: 2px solid var(--tai-color-control-border);
1536
+ border-top-color: var(--tai-color-accent);
1537
+ border-radius: var(--tai-radius-full);
1538
+ animation: tai-spin 0.7s linear infinite;
1539
+ }
1540
+
1541
+ /* The track's ground is barely off the page ground, so its BOUNDARY is what
1542
+ makes the bar visible at all — and a progress bar's outline identifies the
1543
+ component, which rules out the decorative border tier. */
1544
+ .tai-progress-track {
1545
+ position: relative;
1546
+ overflow: hidden;
1547
+ box-sizing: border-box;
1548
+ width: 100%;
1549
+ height: 8px;
1550
+ background: var(--tai-color-surface);
1551
+ border: 1px solid var(--tai-color-control-border);
1552
+ border-radius: var(--tai-radius-full);
1553
+ }
1554
+
1555
+ .tai-progress-fill {
1556
+ height: 100%;
1557
+ background: var(--tai-color-accent);
1558
+ border-radius: var(--tai-radius-full);
1559
+ }
1560
+
1561
+ .tai-progress-fill-indeterminate {
1562
+ width: 30%;
1563
+ animation: tai-progress-indeterminate 1.2s ease-in-out infinite;
1564
+ }
1565
+
1566
+ .tai-empty-state {
1567
+ padding: var(--tai-space-8);
1568
+ text-align: center;
1569
+ font-family: var(--tai-font-sans);
1570
+ font-size: var(--tai-text-md);
1571
+ line-height: 1.5;
1572
+ color: var(--tai-color-text-muted);
1573
+ }
1574
+
1575
+ .tai-empty-state-title {
1576
+ margin: 0;
1577
+ font-size: var(--tai-text-section);
1578
+ line-height: 1.25;
1579
+ }
1580
+
1581
+ /* A server error is arbitrary text: a URL, a token, a base64 body. Without a
1582
+ break rule an unbroken run pushes the page sideways and the tail of the only
1583
+ message the operator has goes off-screen. */
1584
+ .tai-error-state,
1585
+ .tai-warn-state {
1586
+ overflow-wrap: anywhere;
1587
+ }
1588
+
1589
+ .tai-error-state {
1590
+ padding: var(--tai-space-4);
1591
+ background: var(--tai-color-err-tint);
1592
+ border: 1px solid var(--tai-color-err-text);
1593
+ border-radius: var(--tai-radius-md);
1594
+ font-family: var(--tai-font-sans);
1595
+ font-size: var(--tai-text-md);
1596
+ line-height: 1.5;
1597
+ color: var(--tai-color-text);
1598
+ }
1599
+
1600
+ .tai-error-state-title {
1601
+ display: flex;
1602
+ align-items: center;
1603
+ flex-wrap: wrap;
1604
+ gap: var(--tai-space-2);
1605
+ min-width: 0;
1606
+ color: var(--tai-color-err-text);
1607
+ font-weight: 600;
1608
+ }
1609
+
1610
+ /* The same panel one tone down, for a state that degraded rather than failed. */
1611
+ .tai-warn-state {
1612
+ padding: var(--tai-space-4);
1613
+ background: var(--tai-color-warn-tint);
1614
+ border: 1px solid var(--tai-color-warn-text);
1615
+ border-radius: var(--tai-radius-md);
1616
+ font-family: var(--tai-font-sans);
1617
+ font-size: var(--tai-text-md);
1618
+ line-height: 1.5;
1619
+ color: var(--tai-color-text);
1620
+ }
1621
+
1622
+ /* -- Prose --------------------------------------------------------------------
1623
+ The style for HTML the Studio did not author: a registry README injected
1624
+ into a plugin's detail page. The elements a CommonMark + GFM renderer emits
1625
+ have rules here, and the measure is held to a readable line length. GFM's
1626
+ strikethrough (`del`) and task-list checkboxes are NOT styled: they inherit,
1627
+ and a task list additionally shows the `ul` disc beside its checkbox. */
1628
+ .tai-prose {
1629
+ font-family: var(--tai-font-sans);
1630
+ font-size: var(--tai-text-md);
1631
+ line-height: 1.7;
1632
+ color: var(--tai-color-text);
1633
+ overflow-wrap: break-word;
1634
+ }
1635
+
1636
+ .tai-prose > * + * {
1637
+ margin-top: var(--tai-space-4);
1638
+ }
1639
+
1640
+ .tai-prose p,
1641
+ .tai-prose ul,
1642
+ .tai-prose ol,
1643
+ .tai-prose blockquote {
1644
+ max-width: 68ch;
1645
+ }
1646
+
1647
+ .tai-prose h1,
1648
+ .tai-prose h2,
1649
+ .tai-prose h3,
1650
+ .tai-prose h4,
1651
+ .tai-prose h5,
1652
+ .tai-prose h6 {
1653
+ margin: var(--tai-space-6) 0 var(--tai-space-2);
1654
+ }
1655
+
1656
+ .tai-prose h1 {
1657
+ font-size: var(--tai-text-xl);
1658
+ }
1659
+
1660
+ .tai-prose h2 {
1661
+ font-size: var(--tai-text-section);
1662
+ }
1663
+
1664
+ .tai-prose h3 {
1665
+ font-size: var(--tai-text-lg);
1666
+ }
1667
+
1668
+ /* A prose link is always underlined, so color is a reinforcement rather than
1669
+ the only cue. The two prose-link tokens carry the per-theme values (the dark
1670
+ rest state is the LIGHTER crimson, and hover moves to the other member of
1671
+ the ramp in both themes) — see the token file. */
1672
+ .tai-prose a {
1673
+ color: var(--tai-color-prose-link);
1674
+ text-decoration: underline;
1675
+ text-underline-offset: 2px;
1676
+ }
1677
+
1678
+ .tai-prose a:hover {
1679
+ color: var(--tai-color-prose-link-hover);
1680
+ }
1681
+
1682
+ .tai-prose ul,
1683
+ .tai-prose ol {
1684
+ padding-left: var(--tai-space-6);
1685
+ list-style-position: outside;
1686
+ }
1687
+
1688
+ .tai-prose ul {
1689
+ list-style-type: disc;
1690
+ }
1691
+
1692
+ .tai-prose ol {
1693
+ list-style-type: decimal;
1694
+ }
1695
+
1696
+ .tai-prose li + li {
1697
+ margin-top: var(--tai-space-1);
1698
+ }
1699
+
1700
+ .tai-prose code {
1701
+ padding: 0.1em 0.35em;
1702
+ background: var(--tai-color-code-bg);
1703
+ border-radius: var(--tai-radius-code);
1704
+ font-family: var(--tai-font-mono);
1705
+ font-size: var(--tai-text-code);
1706
+ }
1707
+
1708
+ .tai-prose pre code {
1709
+ padding: 0;
1710
+ background: none;
1711
+ font-size: inherit;
1712
+ }
1713
+
1714
+ .tai-prose blockquote {
1715
+ padding-left: var(--tai-space-4);
1716
+ border-left: 3px solid var(--tai-color-border-strong);
1717
+ color: var(--tai-color-text-muted);
1718
+ }
1719
+
1720
+ .tai-prose hr {
1721
+ border: none;
1722
+ border-top: 1px solid var(--tai-color-border);
1723
+ }
1724
+
1725
+ .tai-prose img {
1726
+ max-width: 100%;
1727
+ height: auto;
1728
+ border-radius: var(--tai-radius-md);
1729
+ }
1730
+
1731
+ .tai-prose table {
1732
+ width: 100%;
1733
+ border-collapse: collapse;
1734
+ font-size: var(--tai-text-md);
1735
+ }
1736
+
1737
+ /* A markdown header cell is content, not chrome: it keeps the body's size and
1738
+ the author's own casing, and reads as a header through weight and ink. */
1739
+ .tai-prose th {
1740
+ padding: var(--tai-space-2) var(--tai-space-3);
1741
+ text-align: left;
1742
+ font-weight: 600;
1743
+ color: var(--tai-color-heading);
1744
+ border-bottom: 1px solid var(--tai-color-border-strong);
1745
+ }
1746
+
1747
+ .tai-prose td {
1748
+ padding: var(--tai-space-2) var(--tai-space-3);
1749
+ border-bottom: 1px solid var(--tai-color-border);
1750
+ vertical-align: top;
1751
+ }
1752
+
1753
+ /* -- Page composition ----------------------------------------------------------
1754
+ `.tai-page` is the screen wrapper, `.tai-stack` the vertical rhythm every
1755
+ screen was hand-rolling as a flex column. */
1756
+ .tai-page {
1757
+ box-sizing: border-box;
1758
+ display: flex;
1759
+ flex-direction: column;
1760
+ gap: var(--tai-space-6);
1761
+ width: 100%;
1762
+ max-width: 1440px;
1763
+ margin: 0 auto;
1764
+ padding: var(--tai-space-6) var(--tai-space-8);
1765
+ }
1766
+
1767
+ .tai-stack {
1768
+ display: flex;
1769
+ flex-direction: column;
1770
+ gap: var(--tai-space-4);
1771
+ }
1772
+
1773
+ .tai-stack-2 {
1774
+ gap: var(--tai-space-2);
1775
+ }
1776
+
1777
+ .tai-stack-3 {
1778
+ gap: var(--tai-space-3);
1779
+ }
1780
+
1781
+ .tai-stack-6 {
1782
+ gap: var(--tai-space-6);
1783
+ }
1784
+
1785
+ .tai-row {
1786
+ display: flex;
1787
+ align-items: center;
1788
+ gap: var(--tai-space-3);
1789
+ flex-wrap: wrap;
1790
+ }
1791
+
1792
+ /* A flex item's default `min-width: auto` refuses to shrink below its own
1793
+ content, so server text in a row — an origin, a failure message — pushes the
1794
+ row wider than its container instead of wrapping inside it. */
1795
+ .tai-row > .tai-muted {
1796
+ min-width: 0;
1797
+ overflow-wrap: anywhere;
1798
+ }
1799
+
1800
+ .tai-page-header {
1801
+ display: flex;
1802
+ align-items: flex-start;
1803
+ justify-content: space-between;
1804
+ gap: var(--tai-space-4);
1805
+ flex-wrap: wrap;
1806
+ }
1807
+
1808
+ .tai-page-title {
1809
+ margin: 0;
1810
+ font-size: var(--tai-text-xl);
1811
+ font-weight: 700;
1812
+ line-height: 1.2;
1813
+ color: var(--tai-color-heading);
1814
+ /* A flex item of `.tai-page-header`, holding a caller's own string — a
1815
+ plugin's page name, a server id. Measured at 320-640 px, a 54-character
1816
+ unbroken title took the document to a scroll width of 734 px; the pair
1817
+ lets the title shrink inside the header and break inside the word. */
1818
+ min-width: 0;
1819
+ overflow-wrap: anywhere;
1820
+ }
1821
+
1822
+ /* The rest of the caller-supplied prose: a plugin's card or section heading, a
1823
+ dialog's own title, an empty state's headline, a page's description. None of
1824
+ it is a string this repo owns, so an unbroken run breaks inside the word
1825
+ rather than overflowing its block and pushing the document — the same rule
1826
+ `.tai-error-state` states for arbitrary server text. `break-word` and not
1827
+ `anywhere`: these are blocks, so what they need is a break when the word
1828
+ does not fit the line, and `anywhere` would additionally collapse their
1829
+ min-content, which only a flex item such as `.tai-page-title` needs. */
1830
+ .tai-card-title,
1831
+ .tai-dialog-title,
1832
+ .tai-empty-state-title,
1833
+ .tai-page-description,
1834
+ .tai-section-title {
1835
+ overflow-wrap: break-word;
1836
+ }
1837
+
1838
+ .tai-page-description {
1839
+ margin: var(--tai-space-2) 0 0;
1840
+ max-width: 60ch;
1841
+ font-family: var(--tai-font-sans);
1842
+ font-size: var(--tai-text-md);
1843
+ line-height: 1.5;
1844
+ color: var(--tai-color-text-muted);
1845
+ }
1846
+
1847
+ .tai-page-actions {
1848
+ display: flex;
1849
+ align-items: center;
1850
+ gap: var(--tai-space-2);
1851
+ flex-wrap: wrap;
1852
+ }
1853
+
1854
+ .tai-section-title {
1855
+ margin: 0;
1856
+ font-size: var(--tai-text-section);
1857
+ font-weight: 700;
1858
+ line-height: 1.25;
1859
+ color: var(--tai-color-heading);
1860
+ }
1861
+
1862
+ .tai-kpi {
1863
+ font-size: var(--tai-text-display);
1864
+ font-weight: 700;
1865
+ line-height: 1.1;
1866
+ font-variant-numeric: tabular-nums;
1867
+ color: var(--tai-color-heading);
1868
+ letter-spacing: -0.03em;
1869
+ }
1870
+
1871
+ .tai-muted {
1872
+ color: var(--tai-color-text-muted);
1873
+ }
1874
+
1875
+ .tai-mono {
1876
+ font-family: var(--tai-font-mono);
1877
+ font-variant-numeric: tabular-nums;
1878
+ }
1879
+
1880
+ /* Visually hidden but still announced — the accessible name behind an
1881
+ icon-only segment or control. */
1882
+ .tai-visually-hidden {
1883
+ position: absolute;
1884
+ width: 1px;
1885
+ height: 1px;
1886
+ margin: -1px;
1887
+ padding: 0;
1888
+ overflow: hidden;
1889
+ clip-path: inset(50%);
1890
+ white-space: nowrap;
1891
+ border: 0;
1892
+ }
1893
+
1894
+ /* -- Shell -----------------------------------------------------------------------
1895
+ The application frame: a fixed-width sidebar beside a scrolling main
1896
+ column at >=1024, a 72 px labelled icon rail from 640, and a sticky top bar
1897
+ below 640.
1898
+
1899
+ The bar's height is named once. Three unlinked rules have to agree on it —
1900
+ the bar's own box, the scroll padding that stops it covering an element
1901
+ scrolled to by a focus move, and the scroll margin that stops it covering an
1902
+ anchor jump — and as three bare literals a change to one of them left the
1903
+ other two behind with every gate green. It is NOT a `--tai-*` token: the
1904
+ published contract is `tokens.css`, and this is private to this sheet, so it
1905
+ carries a namespace of its own exactly as that file's own gate requires. */
1906
+ :root {
1907
+ --shell-topbar-height: 56px;
1908
+ }
1909
+
1910
+ .tai-shell {
1911
+ display: grid;
1912
+ grid-template-columns: 232px minmax(0, 1fr);
1913
+ min-height: 100%;
1914
+ background: var(--tai-color-bg);
1915
+ }
1916
+
1917
+ .tai-shell-sidebar {
1918
+ box-sizing: border-box;
1919
+ display: flex;
1920
+ flex-direction: column;
1921
+ gap: var(--tai-space-2);
1922
+ /* A full-viewport sticky column in a TOUCH band, where `vh` is the LARGE
1923
+ viewport: on a phone with a retracting browser chrome the sidebar is taller
1924
+ than the screen by the height of that chrome, and its last nav item sits
1925
+ under it until the user scrolls. `dvh` is the live value. The pair is the
1926
+ fallback, not a preference — `dvh` arrived after this package's floor
1927
+ (Chrome 108, Firefox 101), and a browser below it drops the second
1928
+ declaration and keeps the first. */
1929
+ height: 100vh;
1930
+ height: 100dvh;
1931
+ position: sticky;
1932
+ top: 0;
1933
+ overflow-y: auto;
1934
+ padding: var(--tai-space-4) var(--tai-space-3);
1935
+ background: var(--tai-color-surface);
1936
+ border-right: 1px solid var(--tai-color-border);
1937
+ }
1938
+
1939
+ .tai-shell-main {
1940
+ min-width: 0;
1941
+ background: var(--tai-color-bg);
1942
+ }
1943
+
1944
+ .tai-brand {
1945
+ display: flex;
1946
+ align-items: center;
1947
+ gap: var(--tai-space-2);
1948
+ padding: var(--tai-space-2) var(--tai-space-3);
1949
+ font-size: var(--tai-text-lg);
1950
+ font-weight: 700;
1951
+ letter-spacing: -0.03em;
1952
+ color: var(--tai-color-heading);
1953
+ text-decoration: none;
1954
+ }
1955
+
1956
+ .tai-brand-mark {
1957
+ width: 24px;
1958
+ height: 24px;
1959
+ border-radius: var(--tai-radius-tile);
1960
+ }
1961
+
1962
+ .tai-brand-label {
1963
+ white-space: nowrap;
1964
+ }
1965
+
1966
+ .tai-topbar {
1967
+ display: none;
1968
+ position: sticky;
1969
+ top: 0;
1970
+ z-index: var(--tai-z-sticky);
1971
+ box-sizing: border-box;
1972
+ align-items: center;
1973
+ justify-content: space-between;
1974
+ gap: var(--tai-space-2);
1975
+ height: var(--shell-topbar-height);
1976
+ padding: 0 var(--tai-space-3);
1977
+ background: var(--tai-color-surface);
1978
+ border-bottom: 1px solid var(--tai-color-border);
1979
+ }
1980
+
1981
+ /* -- Master / detail ---------------------------------------------------------------
1982
+ Two fluid panes side by side; below 1024 exactly ONE pane is on screen and
1983
+ the consumer drives which through `data-pane`. */
1984
+ .tai-split {
1985
+ display: grid;
1986
+ grid-template-columns: minmax(0, 1fr) minmax(480px, 1.4fr);
1987
+ gap: var(--tai-space-6);
1988
+ align-items: start;
1989
+ }
1990
+
1991
+ .tai-split-list,
1992
+ .tai-split-detail {
1993
+ min-width: 0;
1994
+ }
1995
+
1996
+ /* -- Card grids ------------------------------------------------------------------- */
1997
+ .tai-grid-cards {
1998
+ display: grid;
1999
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2000
+ gap: var(--tai-space-4);
2001
+ }
2002
+
2003
+ /* -- Responsive bands ---------------------------------------------------------------
2004
+ 1024-1279 medium: the card grid drops to two columns. The sidebar keeps the
2005
+ width it has at every band above 1023, and `.tai-split` is already fluid —
2006
+ its `minmax()` columns hold the detail pane at 480 px or wider. */
2007
+ @media (max-width: 1279px) {
2008
+ .tai-grid-cards {
2009
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2010
+ }
2011
+ }
2012
+
2013
+ /* 640-1023 compact: the sidebar collapses to a 72 px icon rail that KEEPS a
2014
+ short text label under each icon — this is a touch band, never
2015
+ tooltip-only — and only one master/detail pane is on screen. */
2016
+ @media (max-width: 1023px) {
2017
+ .tai-shell {
2018
+ grid-template-columns: 72px minmax(0, 1fr);
2019
+ }
2020
+
2021
+ .tai-shell-sidebar {
2022
+ padding: var(--tai-space-3) var(--tai-space-1);
2023
+ }
2024
+
2025
+ /* SCOPED TO THE RAIL. These three groups only make sense inside the 72 px
2026
+ icon rail the block above builds, so each is a DESCENDANT of
2027
+ `.tai-shell-sidebar`. Unscoped, they reached every `.tai-nav-link` and
2028
+ `.tai-nav-section-header` on the page — including a shell that lays its
2029
+ sidebar out itself and never becomes a rail — and column-stacked its nav
2030
+ labels at 11 px inside a full-width sidebar while hiding every section
2031
+ heading, which is the visible regression the scope prevents. The heading
2032
+ still NAMES its list either way: `aria-labelledby` computes its text from
2033
+ the target's subtree whether or not that target is rendered, so a
2034
+ `display: none` header is a name without a visible label rather than no
2035
+ name — a reason to keep the scope, not the reason. */
2036
+ .tai-shell-sidebar .tai-nav-item,
2037
+ .tai-shell-sidebar .tai-nav-link {
2038
+ flex-direction: column;
2039
+ gap: var(--tai-space-1);
2040
+ padding: var(--tai-space-2) var(--tai-space-1);
2041
+ font-size: var(--tai-text-xs);
2042
+ line-height: 1.2;
2043
+ text-align: center;
2044
+ }
2045
+
2046
+ .tai-shell-sidebar .tai-nav-item[aria-current='page'],
2047
+ .tai-shell-sidebar .tai-nav-link[aria-current='page'] {
2048
+ box-shadow: inset 0 -3px 0 0 var(--tai-color-accent);
2049
+ }
2050
+
2051
+ .tai-shell-sidebar .tai-brand-label,
2052
+ .tai-shell-sidebar .tai-nav-section-header {
2053
+ display: none;
2054
+ }
2055
+
2056
+ .tai-split {
2057
+ grid-template-columns: minmax(0, 1fr);
2058
+ }
2059
+
2060
+ .tai-split[data-pane='list'] > .tai-split-detail,
2061
+ .tai-split[data-pane='detail'] > .tai-split-list {
2062
+ display: none;
2063
+ }
2064
+
2065
+ .tai-page {
2066
+ padding: var(--tai-space-5) var(--tai-space-6);
2067
+ }
2068
+ }
2069
+
2070
+ /* <640 phone: the rail gives way to a sticky 56 px top bar; the shell, the card
2071
+ grid and the page header each fall to a single column, and the page tightens
2072
+ its padding and its gap. */
2073
+ @media (max-width: 639px) {
2074
+ .tai-shell {
2075
+ grid-template-columns: minmax(0, 1fr);
2076
+ }
2077
+
2078
+ .tai-shell-sidebar {
2079
+ display: none;
2080
+ }
2081
+
2082
+ .tai-topbar {
2083
+ display: flex;
2084
+ }
2085
+
2086
+ .tai-page {
2087
+ padding: var(--tai-space-4);
2088
+ gap: var(--tai-space-5);
2089
+ }
2090
+
2091
+ .tai-grid-cards {
2092
+ grid-template-columns: minmax(0, 1fr);
2093
+ }
2094
+
2095
+ .tai-page-header {
2096
+ flex-direction: column;
2097
+ align-items: stretch;
2098
+ }
2099
+ }
2100
+
2101
+ /* The sticky top bar would otherwise cover an element scrolled to by a
2102
+ keyboard focus move or an in-page anchor (WCAG 2.2 — 2.4.11). Padding
2103
+ covers focus scrolling, margin covers anchor jumps. */
2104
+ @media (max-width: 639px) {
2105
+ html {
2106
+ scroll-padding-top: var(--shell-topbar-height);
2107
+ }
2108
+
2109
+ /* Scoped to the jump targets: the margin and the scrollport's padding are
2110
+ ADDITIVE, so putting it on every element with an id would push a focused
2111
+ control twice the bar's height down the page. */
2112
+ :target,
2113
+ #main-content {
2114
+ scroll-margin-top: var(--shell-topbar-height);
2115
+ }
2116
+ }
2117
+
2118
+ /* Touch: the pointer-target minimum wins over the 36 px control height. */
2119
+ @media (pointer: coarse) {
2120
+ .tai-btn,
2121
+ .tai-tab,
2122
+ .tai-nav-item,
2123
+ .tai-nav-link,
2124
+ .tai-choice,
2125
+ /* The pointer-target minimum is for TARGETS. `.tai-chip-static` is a tag or a
2126
+ "+N more" count, not a control (see its rule above), so inflating it to
2127
+ 44 px buys nothing and pushes read-only content around. */
2128
+ .tai-chip:not(.tai-chip-static) {
2129
+ min-height: var(--tai-control-height-coarse);
2130
+ }
2131
+
2132
+ .tai-icon-btn {
2133
+ width: var(--tai-control-height-coarse);
2134
+ height: var(--tai-control-height-coarse);
2135
+ }
2136
+
2137
+ .tai-input,
2138
+ .tai-select-trigger {
2139
+ min-height: var(--tai-control-height-coarse);
2140
+ }
2141
+
2142
+ .tai-select-item,
2143
+ .tai-skip-link {
2144
+ min-height: var(--tai-control-height-coarse);
2145
+ }
2146
+
2147
+ .tai-segment {
2148
+ min-width: var(--tai-control-height-coarse);
2149
+ min-height: var(--tai-control-height-coarse);
2150
+ }
2151
+
2152
+ /* 16 px box + 14 px above and below = the 44 px touch target. The overlay
2153
+ grows VERTICALLY only: a horizontal group sets its options a spacing step
2154
+ apart, and 14 px sideways would reach across that gap into the previous
2155
+ option's label. `.tai-choice`'s own 44 px covers a labelled box. */
2156
+ .tai-checkbox::after,
2157
+ .tai-radio::after {
2158
+ inset-block: -14px;
2159
+ inset-inline: -4px;
2160
+ }
2161
+ }
2162
+
2163
+ /* Reduced motion: nothing animates, nothing lifts. The spinner and the
2164
+ progress bar keep their accessible names, so those two states are still
2165
+ announced without movement. (The skeleton is `aria-hidden` and names
2166
+ nothing either way — it is a visual placeholder only, which is why the rule
2167
+ below keeps it a visible block rather than relying on it being announced.) */
2168
+ @media (prefers-reduced-motion: reduce) {
2169
+ .tai-skeleton::after,
2170
+ .tai-spinner,
2171
+ .tai-progress-fill-indeterminate {
2172
+ animation: none;
2173
+ }
2174
+
2175
+ /* The sweep is what made the block read as pending. Stopping the overlay
2176
+ where it stands would freeze the band across the block, so the overlay
2177
+ goes entirely: what is left is the flat line tone the band rides over, so
2178
+ the block is exactly the moving skeleton with the band taken away — still
2179
+ visibly THERE, and at the same contrast, without moving. */
2180
+ .tai-skeleton::after {
2181
+ display: none;
2182
+ }
2183
+
2184
+ /* Without the sweep, a short fill parked at one end says nothing, so the
2185
+ accent fills the whole track: work is in flight, position unknown. The bar
2186
+ carries no `aria-valuenow`, so it is not read as complete. */
2187
+ .tai-progress-fill-indeterminate {
2188
+ width: 100%;
2189
+ }
2190
+
2191
+ .tai-btn,
2192
+ .tai-icon-btn,
2193
+ .tai-card,
2194
+ .tai-chip,
2195
+ .tai-tab,
2196
+ .tai-nav-item,
2197
+ .tai-nav-link,
2198
+ .tai-segment,
2199
+ .tai-input,
2200
+ .tai-textarea,
2201
+ .tai-select-trigger,
2202
+ .tai-skip-link {
2203
+ transition: none;
2204
+ }
2205
+
2206
+ .tai-btn:hover:not(:disabled):not([aria-disabled='true']),
2207
+ .tai-card-interactive:hover,
2208
+ .tai-card-interactive:focus-within {
2209
+ transform: none;
2210
+ }
2211
+ }
2212
+ }