@tai42/studio-sdk 16.0.0 → 16.0.1

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 (317) hide show
  1. package/dist/components/explorer-body.d.ts +49 -0
  2. package/dist/components/explorer-body.d.ts.map +1 -0
  3. package/dist/components/explorer-body.js +67 -0
  4. package/dist/components/explorer-body.js.map +1 -0
  5. package/dist/components/explorer-page-size.d.ts +4 -0
  6. package/dist/components/explorer-page-size.d.ts.map +1 -0
  7. package/dist/components/explorer-page-size.js +36 -0
  8. package/dist/components/explorer-page-size.js.map +1 -0
  9. package/dist/components/explorer-tag-filter.d.ts +17 -0
  10. package/dist/components/explorer-tag-filter.d.ts.map +1 -0
  11. package/dist/components/explorer-tag-filter.js +29 -0
  12. package/dist/components/explorer-tag-filter.js.map +1 -0
  13. package/dist/components/explorer-tags.d.ts +22 -0
  14. package/dist/components/explorer-tags.d.ts.map +1 -0
  15. package/dist/components/explorer-tags.js +51 -0
  16. package/dist/components/explorer-tags.js.map +1 -0
  17. package/dist/components/explorer-view.d.ts +3 -17
  18. package/dist/components/explorer-view.d.ts.map +1 -1
  19. package/dist/components/explorer-view.js +53 -170
  20. package/dist/components/explorer-view.js.map +1 -1
  21. package/dist/components/extension-combo-builder.d.ts +0 -5
  22. package/dist/components/extension-combo-builder.d.ts.map +1 -1
  23. package/dist/components/extension-combo-builder.js +4 -42
  24. package/dist/components/extension-combo-builder.js.map +1 -1
  25. package/dist/components/extension-combo-parts.d.ts +44 -0
  26. package/dist/components/extension-combo-parts.d.ts.map +1 -0
  27. package/dist/components/extension-combo-parts.js +57 -0
  28. package/dist/components/extension-combo-parts.js.map +1 -0
  29. package/dist/components/fleet-report.d.ts.map +1 -1
  30. package/dist/components/fleet-report.js +51 -37
  31. package/dist/components/fleet-report.js.map +1 -1
  32. package/dist/components/icons/chrome-marks.d.ts +50 -0
  33. package/dist/components/icons/chrome-marks.d.ts.map +1 -0
  34. package/dist/components/icons/chrome-marks.js +51 -0
  35. package/dist/components/icons/chrome-marks.js.map +1 -0
  36. package/dist/components/icons/icon-frame.d.ts +29 -0
  37. package/dist/components/icons/icon-frame.d.ts.map +1 -0
  38. package/dist/components/icons/icon-frame.js +19 -0
  39. package/dist/components/icons/icon-frame.js.map +1 -0
  40. package/dist/components/icons/index.d.ts +40 -0
  41. package/dist/components/icons/index.d.ts.map +1 -0
  42. package/dist/components/icons/index.js +6 -0
  43. package/dist/components/icons/index.js.map +1 -0
  44. package/dist/components/icons/nav-map.d.ts +14 -0
  45. package/dist/components/icons/nav-map.d.ts.map +1 -0
  46. package/dist/components/icons/nav-map.js +28 -0
  47. package/dist/components/icons/nav-map.js.map +1 -0
  48. package/dist/components/icons/nav-marks.d.ts +44 -0
  49. package/dist/components/icons/nav-marks.d.ts.map +1 -0
  50. package/dist/components/icons/nav-marks.js +45 -0
  51. package/dist/components/icons/nav-marks.js.map +1 -0
  52. package/dist/components/icons/status-marks.d.ts +21 -0
  53. package/dist/components/icons/status-marks.d.ts.map +1 -0
  54. package/dist/components/icons/status-marks.js +22 -0
  55. package/dist/components/icons/status-marks.js.map +1 -0
  56. package/dist/components/icons/theme-marks.d.ts +9 -0
  57. package/dist/components/icons/theme-marks.d.ts.map +1 -0
  58. package/dist/components/icons/theme-marks.js +10 -0
  59. package/dist/components/icons/theme-marks.js.map +1 -0
  60. package/dist/components/json-diff.d.ts.map +1 -1
  61. package/dist/components/json-diff.js +1 -3
  62. package/dist/components/json-diff.js.map +1 -1
  63. package/dist/components/json-tree-context.d.ts +13 -0
  64. package/dist/components/json-tree-context.d.ts.map +1 -0
  65. package/dist/components/json-tree-context.js +15 -0
  66. package/dist/components/json-tree-context.js.map +1 -0
  67. package/dist/components/json-tree-model.d.ts +56 -0
  68. package/dist/components/json-tree-model.d.ts.map +1 -0
  69. package/dist/components/json-tree-model.js +125 -0
  70. package/dist/components/json-tree-model.js.map +1 -0
  71. package/dist/components/json-tree-nodes.d.ts +30 -0
  72. package/dist/components/json-tree-nodes.d.ts.map +1 -0
  73. package/dist/components/json-tree-nodes.js +118 -0
  74. package/dist/components/json-tree-nodes.js.map +1 -0
  75. package/dist/components/json-tree.d.ts.map +1 -1
  76. package/dist/components/json-tree.js +7 -295
  77. package/dist/components/json-tree.js.map +1 -1
  78. package/dist/components/json-value-format.d.ts +20 -0
  79. package/dist/components/json-value-format.d.ts.map +1 -0
  80. package/dist/components/json-value-format.js +63 -0
  81. package/dist/components/json-value-format.js.map +1 -0
  82. package/dist/components/markdown-blocks.d.ts +38 -0
  83. package/dist/components/markdown-blocks.d.ts.map +1 -0
  84. package/dist/components/markdown-blocks.js +168 -0
  85. package/dist/components/markdown-blocks.js.map +1 -0
  86. package/dist/components/markdown-inline.d.ts +24 -0
  87. package/dist/components/markdown-inline.d.ts.map +1 -0
  88. package/dist/components/markdown-inline.js +257 -0
  89. package/dist/components/markdown-inline.js.map +1 -0
  90. package/dist/components/markdown.d.ts +3 -52
  91. package/dist/components/markdown.d.ts.map +1 -1
  92. package/dist/components/markdown.js +4 -376
  93. package/dist/components/markdown.js.map +1 -1
  94. package/dist/components/observer-registry.d.ts +38 -0
  95. package/dist/components/observer-registry.d.ts.map +1 -0
  96. package/dist/components/observer-registry.js +131 -0
  97. package/dist/components/observer-registry.js.map +1 -0
  98. package/dist/components/overflow-measure.d.ts +38 -0
  99. package/dist/components/overflow-measure.d.ts.map +1 -0
  100. package/dist/components/overflow-measure.js +54 -0
  101. package/dist/components/overflow-measure.js.map +1 -0
  102. package/dist/components/prose-regions.d.ts +81 -0
  103. package/dist/components/prose-regions.d.ts.map +1 -0
  104. package/dist/components/prose-regions.js +128 -0
  105. package/dist/components/prose-regions.js.map +1 -0
  106. package/dist/components/scroll-region.d.ts.map +1 -1
  107. package/dist/components/scroll-region.js +3 -298
  108. package/dist/components/scroll-region.js.map +1 -1
  109. package/dist/components/tab-stop-parse.d.ts +53 -0
  110. package/dist/components/tab-stop-parse.d.ts.map +1 -0
  111. package/dist/components/tab-stop-parse.js +202 -0
  112. package/dist/components/tab-stop-parse.js.map +1 -0
  113. package/dist/components/templated-text-field.d.ts.map +1 -1
  114. package/dist/components/templated-text-field.js +10 -148
  115. package/dist/components/templated-text-field.js.map +1 -1
  116. package/dist/components/templated-text-kwargs-editor.d.ts +24 -0
  117. package/dist/components/templated-text-kwargs-editor.d.ts.map +1 -0
  118. package/dist/components/templated-text-kwargs-editor.js +48 -0
  119. package/dist/components/templated-text-kwargs-editor.js.map +1 -0
  120. package/dist/components/templated-text-kwargs.d.ts +37 -0
  121. package/dist/components/templated-text-kwargs.d.ts.map +1 -0
  122. package/dist/components/templated-text-kwargs.js +66 -0
  123. package/dist/components/templated-text-kwargs.js.map +1 -0
  124. package/dist/components/templated-text-source.d.ts +33 -0
  125. package/dist/components/templated-text-source.d.ts.map +1 -0
  126. package/dist/components/templated-text-source.js +53 -0
  127. package/dist/components/templated-text-source.js.map +1 -0
  128. package/dist/components/tool-picker-parts.d.ts +39 -0
  129. package/dist/components/tool-picker-parts.d.ts.map +1 -0
  130. package/dist/components/tool-picker-parts.js +87 -0
  131. package/dist/components/tool-picker-parts.js.map +1 -0
  132. package/dist/components/tool-picker.d.ts +1 -2
  133. package/dist/components/tool-picker.d.ts.map +1 -1
  134. package/dist/components/tool-picker.js +4 -66
  135. package/dist/components/tool-picker.js.map +1 -1
  136. package/dist/components/version-history-panel.d.ts.map +1 -1
  137. package/dist/components/version-history-panel.js +7 -34
  138. package/dist/components/version-history-panel.js.map +1 -1
  139. package/dist/components/version-history-rows.d.ts +48 -0
  140. package/dist/components/version-history-rows.d.ts.map +1 -0
  141. package/dist/components/version-history-rows.js +51 -0
  142. package/dist/components/version-history-rows.js.map +1 -0
  143. package/dist/guards.d.ts +6 -0
  144. package/dist/guards.d.ts.map +1 -0
  145. package/dist/guards.js +8 -0
  146. package/dist/guards.js.map +1 -0
  147. package/dist/hooks/interactions-connection.d.ts +43 -0
  148. package/dist/hooks/interactions-connection.d.ts.map +1 -0
  149. package/dist/hooks/interactions-connection.js +171 -0
  150. package/dist/hooks/interactions-connection.js.map +1 -0
  151. package/dist/hooks/interactions-stream.d.ts +80 -0
  152. package/dist/hooks/interactions-stream.d.ts.map +1 -0
  153. package/dist/hooks/interactions-stream.js +233 -0
  154. package/dist/hooks/interactions-stream.js.map +1 -0
  155. package/dist/hooks/useApi.d.ts +2192 -2192
  156. package/dist/hooks/useApi.d.ts.map +1 -1
  157. package/dist/hooks/useSse.d.ts +3 -6
  158. package/dist/hooks/useSse.d.ts.map +1 -1
  159. package/dist/hooks/useSse.js +38 -451
  160. package/dist/hooks/useSse.js.map +1 -1
  161. package/dist/navigation/app-link.d.ts +25 -0
  162. package/dist/navigation/app-link.d.ts.map +1 -0
  163. package/dist/navigation/app-link.js +33 -0
  164. package/dist/navigation/app-link.js.map +1 -0
  165. package/dist/navigation/context.d.ts +3 -20
  166. package/dist/navigation/context.d.ts.map +1 -1
  167. package/dist/navigation/context.js +7 -267
  168. package/dist/navigation/context.js.map +1 -1
  169. package/dist/navigation/entry-state.d.ts +23 -0
  170. package/dist/navigation/entry-state.d.ts.map +1 -0
  171. package/dist/navigation/entry-state.js +39 -0
  172. package/dist/navigation/entry-state.js.map +1 -0
  173. package/dist/navigation/guard-registry.d.ts +30 -0
  174. package/dist/navigation/guard-registry.d.ts.map +1 -0
  175. package/dist/navigation/guard-registry.js +46 -0
  176. package/dist/navigation/guard-registry.js.map +1 -0
  177. package/dist/navigation/guarded-navigation.d.ts +24 -0
  178. package/dist/navigation/guarded-navigation.d.ts.map +1 -0
  179. package/dist/navigation/guarded-navigation.js +102 -0
  180. package/dist/navigation/guarded-navigation.js.map +1 -0
  181. package/dist/navigation/index.d.ts +3 -2
  182. package/dist/navigation/index.d.ts.map +1 -1
  183. package/dist/navigation/index.js +2 -1
  184. package/dist/navigation/index.js.map +1 -1
  185. package/dist/navigation/types.d.ts +8 -11
  186. package/dist/navigation/types.d.ts.map +1 -1
  187. package/dist/navigation/use-back-forward-guard.d.ts +5 -0
  188. package/dist/navigation/use-back-forward-guard.d.ts.map +1 -0
  189. package/dist/navigation/use-back-forward-guard.js +82 -0
  190. package/dist/navigation/use-back-forward-guard.js.map +1 -0
  191. package/dist/plugin/registry.d.ts +6 -15
  192. package/dist/plugin/registry.d.ts.map +1 -1
  193. package/dist/plugin/registry.js +83 -75
  194. package/dist/plugin/registry.js.map +1 -1
  195. package/dist/schema-form/SecretRefField.d.ts +2 -3
  196. package/dist/schema-form/SecretRefField.d.ts.map +1 -1
  197. package/dist/schema-form/SecretRefField.js +23 -12
  198. package/dist/schema-form/SecretRefField.js.map +1 -1
  199. package/dist/schema-form/classify.d.ts +1 -122
  200. package/dist/schema-form/classify.d.ts.map +1 -1
  201. package/dist/schema-form/classify.js +174 -237
  202. package/dist/schema-form/classify.js.map +1 -1
  203. package/dist/schema-form/default-value.d.ts.map +1 -1
  204. package/dist/schema-form/default-value.js +42 -19
  205. package/dist/schema-form/default-value.js.map +1 -1
  206. package/dist/schema-form/enum-field.d.ts +1 -1
  207. package/dist/schema-form/enum-field.d.ts.map +1 -1
  208. package/dist/schema-form/field-model.d.ts +121 -0
  209. package/dist/schema-form/field-model.d.ts.map +1 -0
  210. package/dist/schema-form/field-model.js +2 -0
  211. package/dist/schema-form/field-model.js.map +1 -0
  212. package/dist/schema-form/field-node.d.ts.map +1 -1
  213. package/dist/schema-form/field-node.js +25 -14
  214. package/dist/schema-form/field-node.js.map +1 -1
  215. package/dist/schema-form/media-field.d.ts +1 -1
  216. package/dist/schema-form/media-field.d.ts.map +1 -1
  217. package/dist/schema-form/media-field.js +47 -28
  218. package/dist/schema-form/media-field.js.map +1 -1
  219. package/dist/schema-form/string-annotations.d.ts +20 -0
  220. package/dist/schema-form/string-annotations.d.ts.map +1 -0
  221. package/dist/schema-form/string-annotations.js +105 -0
  222. package/dist/schema-form/string-annotations.js.map +1 -0
  223. package/dist/schema-form/string-field.d.ts +1 -1
  224. package/dist/schema-form/string-field.d.ts.map +1 -1
  225. package/dist/schema-form/union-field.d.ts +1 -1
  226. package/dist/schema-form/union-field.d.ts.map +1 -1
  227. package/dist/schema-form/union-field.js.map +1 -1
  228. package/dist/schema-form/union.d.ts +1 -1
  229. package/dist/schema-form/union.d.ts.map +1 -1
  230. package/dist/schema-form/validate.d.ts.map +1 -1
  231. package/dist/schema-form/validate.js +126 -105
  232. package/dist/schema-form/validate.js.map +1 -1
  233. package/dist/state-binding/AdapterMapping.d.ts.map +1 -1
  234. package/dist/state-binding/AdapterMapping.js +0 -0
  235. package/dist/state-binding/AdapterMapping.js.map +1 -1
  236. package/dist/state-binding/StateAttachRow.d.ts.map +1 -1
  237. package/dist/state-binding/StateAttachRow.js +52 -36
  238. package/dist/state-binding/StateAttachRow.js.map +1 -1
  239. package/dist/state-binding/adapter-mapping-row.d.ts +17 -0
  240. package/dist/state-binding/adapter-mapping-row.d.ts.map +1 -0
  241. package/dist/state-binding/adapter-mapping-row.js +87 -0
  242. package/dist/state-binding/adapter-mapping-row.js.map +1 -0
  243. package/dist/structured-output/StructuredOutput.d.ts.map +1 -1
  244. package/dist/structured-output/StructuredOutput.js +1 -3
  245. package/dist/structured-output/StructuredOutput.js.map +1 -1
  246. package/package.json +2 -2
  247. package/src/components/explorer-body.tsx +239 -0
  248. package/src/components/explorer-page-size.ts +39 -0
  249. package/src/components/explorer-tag-filter.tsx +80 -0
  250. package/src/components/explorer-tags.ts +64 -0
  251. package/src/components/explorer-view.tsx +114 -366
  252. package/src/components/extension-combo-builder.tsx +35 -152
  253. package/src/components/extension-combo-parts.tsx +237 -0
  254. package/src/components/fleet-report.tsx +97 -54
  255. package/src/components/icons/chrome-marks.tsx +195 -0
  256. package/src/components/icons/icon-frame.tsx +50 -0
  257. package/src/components/icons/index.ts +84 -0
  258. package/src/components/icons/nav-map.ts +55 -0
  259. package/src/components/icons/nav-marks.tsx +205 -0
  260. package/src/components/icons/status-marks.tsx +46 -0
  261. package/src/components/icons/theme-marks.tsx +33 -0
  262. package/src/components/json-diff.tsx +1 -4
  263. package/src/components/json-tree-context.ts +26 -0
  264. package/src/components/json-tree-model.ts +139 -0
  265. package/src/components/json-tree-nodes.tsx +224 -0
  266. package/src/components/json-tree.tsx +15 -434
  267. package/src/components/json-value-format.ts +64 -0
  268. package/src/components/markdown-blocks.ts +198 -0
  269. package/src/components/markdown-inline.tsx +365 -0
  270. package/src/components/markdown.tsx +5 -471
  271. package/src/components/observer-registry.ts +139 -0
  272. package/src/components/overflow-measure.ts +69 -0
  273. package/src/components/prose-regions.ts +153 -0
  274. package/src/components/scroll-region.tsx +20 -360
  275. package/src/components/tab-stop-parse.ts +217 -0
  276. package/src/components/templated-text-field.tsx +38 -330
  277. package/src/components/templated-text-kwargs-editor.tsx +147 -0
  278. package/src/components/templated-text-kwargs.ts +92 -0
  279. package/src/components/templated-text-source.tsx +226 -0
  280. package/src/components/tool-picker-parts.tsx +184 -0
  281. package/src/components/tool-picker.tsx +22 -114
  282. package/src/components/version-history-panel.tsx +38 -142
  283. package/src/components/version-history-rows.tsx +220 -0
  284. package/src/guards.ts +7 -0
  285. package/src/hooks/interactions-connection.ts +227 -0
  286. package/src/hooks/interactions-stream.ts +274 -0
  287. package/src/hooks/useSse.ts +50 -477
  288. package/src/navigation/app-link.tsx +70 -0
  289. package/src/navigation/context.tsx +11 -330
  290. package/src/navigation/entry-state.ts +42 -0
  291. package/src/navigation/guard-registry.ts +65 -0
  292. package/src/navigation/guarded-navigation.ts +147 -0
  293. package/src/navigation/index.ts +2 -2
  294. package/src/navigation/types.ts +8 -11
  295. package/src/navigation/use-back-forward-guard.ts +90 -0
  296. package/src/plugin/registry.ts +111 -90
  297. package/src/schema-form/SecretRefField.tsx +121 -71
  298. package/src/schema-form/classify.ts +244 -395
  299. package/src/schema-form/default-value.ts +48 -17
  300. package/src/schema-form/enum-field.tsx +1 -1
  301. package/src/schema-form/field-model.ts +133 -0
  302. package/src/schema-form/field-node.tsx +94 -38
  303. package/src/schema-form/media-field.tsx +134 -65
  304. package/src/schema-form/string-annotations.ts +111 -0
  305. package/src/schema-form/string-field.tsx +1 -1
  306. package/src/schema-form/union-field.tsx +2 -1
  307. package/src/schema-form/union.ts +1 -1
  308. package/src/schema-form/validate.ts +184 -103
  309. package/src/state-binding/AdapterMapping.tsx +0 -0
  310. package/src/state-binding/StateAttachRow.tsx +222 -147
  311. package/src/state-binding/adapter-mapping-row.tsx +232 -0
  312. package/src/structured-output/StructuredOutput.tsx +1 -4
  313. package/dist/components/icons.d.ts +0 -160
  314. package/dist/components/icons.d.ts.map +0 -1
  315. package/dist/components/icons.js +0 -160
  316. package/dist/components/icons.js.map +0 -1
  317. package/src/components/icons.tsx +0 -586
@@ -0,0 +1,217 @@
1
+ /**
2
+ * The low-level parsing behind `tabStopsIn`: balanced-bracket and value scanning,
3
+ * a tag's attribute value, the binding/import RESOLVER (name → its home file and
4
+ * definition, expanded across hops), and the two per-tag predicates —
5
+ * `tagDeclaresTab` (is this a tab stop?) and `taiClassesOf` (which `tai-*` classes
6
+ * does it wear?).
7
+ */
8
+
9
+ /**
10
+ * The text from `start` to the character closing the bracket it opens, quotes
11
+ * respected so a brace inside a string neither opens nor closes anything.
12
+ */
13
+ export function balancedFrom(source: string, start: number, open: string, close: string): string {
14
+ let depth = 0;
15
+ let quote: string | undefined;
16
+ for (let index = start; index < source.length; index += 1) {
17
+ const character = source[index];
18
+ if (quote !== undefined) {
19
+ if (character === '\\') {
20
+ index += 1;
21
+ continue;
22
+ }
23
+ if (character === quote) quote = undefined;
24
+ continue;
25
+ }
26
+ if (character === '"' || character === "'" || character === '`') {
27
+ quote = character;
28
+ continue;
29
+ }
30
+ if (character === open) depth += 1;
31
+ else if (character === close) {
32
+ depth -= 1;
33
+ if (depth === 0) return source.slice(start, index + 1);
34
+ }
35
+ }
36
+ return source.slice(start);
37
+ }
38
+
39
+ /**
40
+ * The RHS of a `const`/`let` assignment beginning at `start`: text up to the
41
+ * top-level `;`, with brackets and quotes tracked so a `;` inside either does not
42
+ * end it.
43
+ */
44
+ const QUOTE_CHARS = '"\'`';
45
+ const OPEN_BRACKETS = '{([';
46
+ const CLOSE_BRACKETS = '})]';
47
+
48
+ export function readAssignmentValue(source: string, start: number): string {
49
+ let end = start;
50
+ let depth = 0;
51
+ let quote: string | undefined;
52
+ for (; end < source.length; end += 1) {
53
+ const character = source[end] ?? '';
54
+ if (quote !== undefined) {
55
+ if (character === '\\') end += 1;
56
+ else if (character === quote) quote = undefined;
57
+ continue;
58
+ }
59
+ if (QUOTE_CHARS.includes(character)) quote = character;
60
+ else if (OPEN_BRACKETS.includes(character)) depth += 1;
61
+ else if (CLOSE_BRACKETS.includes(character)) depth -= 1;
62
+ else if (character === ';' && depth === 0) break;
63
+ }
64
+ return source.slice(start, end);
65
+ }
66
+
67
+ /**
68
+ * The index of the `>` closing an opening tag whose attribute run starts at
69
+ * `from`, quotes and brace depth tracked so a `>` inside a `className` expression
70
+ * (`{x > 0 ? a : b}`) is not mistaken for it.
71
+ */
72
+ export function scanToTagClose(source: string, from: number): number {
73
+ let end = from;
74
+ let depth = 0;
75
+ let quote: string | undefined;
76
+ for (; end < source.length; end += 1) {
77
+ const character = source[end];
78
+ if (quote !== undefined) {
79
+ if (character === '\\') end += 1;
80
+ else if (character === quote) quote = undefined;
81
+ continue;
82
+ }
83
+ if (character === '"' || character === "'" || character === '`') quote = character;
84
+ else if (character === '{') depth += 1;
85
+ else if (character === '}') depth -= 1;
86
+ else if (character === '>' && depth === 0) break;
87
+ }
88
+ return end;
89
+ }
90
+
91
+ /** One opening JSX tag: its element name and everything between that and the `>`. */
92
+ export interface OpeningTag {
93
+ readonly name: string;
94
+ readonly attributes: string;
95
+ }
96
+
97
+ /**
98
+ * The value written for `attribute`, unquoted or unbraced; `undefined` when the
99
+ * tag does not write it at all, which is the difference between "no `tabIndex`"
100
+ * and "`tabIndex={-1}`".
101
+ */
102
+ export function attributeValue(attributes: string, attribute: string): string | undefined {
103
+ const written = new RegExp(String.raw`(?<![\w-])${attribute}\s*=`).exec(attributes);
104
+ if (written === null) return undefined;
105
+ let index = written.index + written[0].length;
106
+ while (/\s/.test(attributes[index] ?? '')) index += 1;
107
+ const opener = attributes[index];
108
+ if (opener === '"' || opener === "'") {
109
+ const close = attributes.indexOf(opener, index + 1);
110
+ return attributes.slice(index + 1, close === -1 ? attributes.length : close);
111
+ }
112
+ if (opener === '{') return balancedFrom(attributes, index, '{', '}').slice(1, -1);
113
+ return '';
114
+ }
115
+
116
+ /** The elements that are in the tab order with no `tabIndex` of their own. */
117
+ const NATIVELY_FOCUSABLE = new Set(['button', 'a', 'input', 'textarea', 'select', 'summary']);
118
+
119
+ /** A `tabIndex` that takes the element OUT of the tab order. */
120
+ const NEGATIVE_TAB_INDEX = /^\s*-/;
121
+
122
+ /** A props object that puts its target in the tab order. */
123
+ const SPREADS_TAB_INDEX = /tabIndex\s*:\s*(?!\s*-)/;
124
+
125
+ /** Resolves a referenced name to the file it is bound in and its definition text. */
126
+ export interface TabStopResolver {
127
+ /** Where a name is bound: the file itself, or the file it is imported from. */
128
+ homeOf(file: string, name: string): string | undefined;
129
+ /** The definition text of `name` bound in `home`, if any. */
130
+ definitionOf(home: string, name: string): string | undefined;
131
+ /**
132
+ * `text` with the definition of every name it references appended, to `depth`
133
+ * hops — a class reaches its element through as many as three (the tag names a
134
+ * helper, the helper names a table, the table holds the literal).
135
+ */
136
+ expand(text: string, file: string, depth: number, seen: Set<string>): string;
137
+ }
138
+
139
+ /** A resolver over pre-scanned per-file binding and import maps. */
140
+ export function createTabStopResolver(
141
+ bindings: ReadonlyMap<string, Map<string, string>>,
142
+ imports: ReadonlyMap<string, Map<string, string>>,
143
+ ): TabStopResolver {
144
+ function homeOf(file: string, name: string): string | undefined {
145
+ if (bindings.get(file)?.has(name) === true) return file;
146
+ return imports.get(file)?.get(name);
147
+ }
148
+
149
+ function definitionOf(home: string, name: string): string | undefined {
150
+ return bindings.get(home)?.get(name);
151
+ }
152
+
153
+ function expand(text: string, file: string, depth: number, seen: Set<string>): string {
154
+ if (depth === 0) return text;
155
+ let out = text;
156
+ for (const match of text.matchAll(/(?<![\w.$])([A-Za-z_$][\w$]*)/g)) {
157
+ const name = match[1] ?? '';
158
+ const home = homeOf(file, name);
159
+ if (home === undefined) continue;
160
+ const key = `${home}#${name}`;
161
+ if (seen.has(key)) continue;
162
+ seen.add(key);
163
+ const definition = definitionOf(home, name);
164
+ if (definition !== undefined) out += `\n${expand(definition, home, depth - 1, seen)}`;
165
+ }
166
+ return out;
167
+ }
168
+
169
+ return { homeOf, definitionOf, expand };
170
+ }
171
+
172
+ /** Whether a spread props object on the tag declares a non-negative `tabIndex`. */
173
+ function spreadDeclaresTab(tag: OpeningTag, file: string, resolver: TabStopResolver): boolean {
174
+ for (const spread of tag.attributes.matchAll(/\{\s*\.\.\.\s*([A-Za-z_$][\w$]*)/g)) {
175
+ const home = resolver.homeOf(file, spread[1] ?? '');
176
+ if (home === undefined) continue;
177
+ const definition = resolver.definitionOf(home, spread[1] ?? '');
178
+ if (
179
+ definition !== undefined &&
180
+ SPREADS_TAB_INDEX.test(resolver.expand(definition, home, 2, new Set()))
181
+ ) {
182
+ return true;
183
+ }
184
+ }
185
+ return false;
186
+ }
187
+
188
+ /**
189
+ * Whether the tag is a keyboard tab stop: a natively focusable element, an element
190
+ * with a non-negative `tabIndex`, or one spreading a props object that declares one.
191
+ */
192
+ export function tagDeclaresTab(tag: OpeningTag, file: string, resolver: TabStopResolver): boolean {
193
+ const native =
194
+ NATIVELY_FOCUSABLE.has(tag.name) &&
195
+ (tag.name !== 'a' || /(?<![\w-])href\s*=/.test(tag.attributes));
196
+ if (native) return true;
197
+ const written = attributeValue(tag.attributes, 'tabIndex');
198
+ if (written !== undefined && !NEGATIVE_TAB_INDEX.test(written)) return true;
199
+ return spreadDeclaresTab(tag, file, resolver);
200
+ }
201
+
202
+ /** The `tai-*` classes the tag wears, expanded through its bindings to `depth` 3. */
203
+ export function taiClassesOf(tag: OpeningTag, file: string, resolver: TabStopResolver): string[] {
204
+ const className = attributeValue(tag.attributes, 'className');
205
+ if (className === undefined) return [];
206
+ return [
207
+ ...new Set(
208
+ [
209
+ ...resolver
210
+ .expand(className, file, 3, new Set())
211
+ .matchAll(/(?<![\w-])(tai-[a-z][\w-]*)(?![\w])/g),
212
+ ]
213
+ .map((match) => `.${match[1] ?? ''}`)
214
+ .filter((name) => !name.endsWith('-')),
215
+ ),
216
+ ];
217
+ }
@@ -28,12 +28,16 @@ import { useState, type ReactNode } from 'react';
28
28
 
29
29
  import type { TemplatedText } from '@tai42/api-client';
30
30
 
31
- import { Button, ErrorState, Skeleton } from './primitives';
32
31
  import { Field } from './field';
33
- import { RadioGroup } from './radio-group';
34
- import { Select } from './select';
35
- import { Textarea, TextInput } from './inputs';
36
- import { CloseIcon } from './icons';
32
+ import {
33
+ initialMode,
34
+ objectToRows,
35
+ templatedValueFrom,
36
+ type KwargRow,
37
+ type Mode,
38
+ } from './templated-text-kwargs';
39
+ import { KwargsEditor, ReadOnlyKwargs } from './templated-text-kwargs-editor';
40
+ import { TemplatedTextSource } from './templated-text-source';
37
41
 
38
42
  /** One stored template the id picker offers. */
39
43
  export interface TemplatedTextTemplateOption {
@@ -111,170 +115,6 @@ export function templatedTextSummary(value: TemplatedText | null): string {
111
115
  return value.content ?? '';
112
116
  }
113
117
 
114
- type Mode = 'inline' | 'stored';
115
-
116
- interface KwargRow {
117
- readonly key: string;
118
- readonly value: string;
119
- /**
120
- * The stored value this row was seeded from, kept so an UNTOUCHED row re-emits it
121
- * BYTE-FOR-BYTE: the row form cannot tell a stored string `"7"` from the number `7`,
122
- * so re-parsing an untouched row would silently coerce the stored type. `undefined`
123
- * for a row the author added.
124
- */
125
- readonly original?: unknown;
126
- }
127
-
128
- /** The row's canonical string form, matched against `value` to detect an untouched row. */
129
- function stringifyRowValue(raw: unknown): string {
130
- return typeof raw === 'string' ? raw : JSON.stringify(raw);
131
- }
132
-
133
- /** Parse a cell as JSON when it can be (numbers/bools/objects), else keep the string. */
134
- function parseCellValue(raw: string): unknown {
135
- const trimmed = raw.trim();
136
- if (trimmed.length === 0) return '';
137
- try {
138
- return JSON.parse(trimmed);
139
- } catch {
140
- return raw;
141
- }
142
- }
143
-
144
- /** Collapse the kwargs rows into an object, dropping rows with a blank key. */
145
- function rowsToObject(rows: readonly KwargRow[]): Record<string, unknown> {
146
- const out: Record<string, unknown> = {};
147
- for (const row of rows) {
148
- const key = row.key.trim();
149
- if (key.length === 0) continue;
150
- // An untouched seeded row re-emits its stored value verbatim (no JSON-type coerce);
151
- // an edited or added row is parsed from the cell text.
152
- out[key] =
153
- row.original !== undefined && stringifyRowValue(row.original) === row.value
154
- ? row.original
155
- : parseCellValue(row.value);
156
- }
157
- return out;
158
- }
159
-
160
- /** Seed rows from a stored kwargs object (stringifying non-string values). */
161
- function objectToRows(kwargs: Record<string, unknown> | undefined): KwargRow[] {
162
- if (kwargs === undefined) return [];
163
- return Object.entries(kwargs).map(([key, raw]) => ({
164
- key,
165
- value: stringifyRowValue(raw),
166
- original: raw,
167
- }));
168
- }
169
-
170
- /** The add/remove kwargs editor, revealed on demand and open when kwargs already exist. */
171
- function KwargsEditor({
172
- label,
173
- rows,
174
- disabled,
175
- onChange,
176
- }: {
177
- readonly label: string;
178
- readonly rows: readonly KwargRow[];
179
- readonly disabled: boolean;
180
- readonly onChange: (rows: KwargRow[]) => void;
181
- }): ReactNode {
182
- const [open, setOpen] = useState(rows.length > 0);
183
- const groupLabel = `${label} render parameters`;
184
-
185
- if (!open) {
186
- // Wrapped so the ghost button keeps its content width and stays left-aligned
187
- // in the field's column, marking it as this field's own disclosure rather than
188
- // a full-width control floating between two fields.
189
- return (
190
- <div>
191
- <Button
192
- type="button"
193
- variant="ghost"
194
- disabled={disabled}
195
- onClick={() => {
196
- setOpen(true);
197
- onChange([...rows, { key: '', value: '' }]);
198
- }}
199
- >
200
- Add render parameters
201
- </Button>
202
- </div>
203
- );
204
- }
205
-
206
- return (
207
- <div
208
- role="group"
209
- aria-label={groupLabel}
210
- style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-2)' }}
211
- >
212
- <span style={{ fontSize: 'var(--tai-text-sm)', color: 'var(--tai-color-text-muted)' }}>
213
- Render parameters
214
- </span>
215
- {rows.map((row, index) => (
216
- <div
217
- key={index}
218
- style={{ display: 'flex', gap: 'var(--tai-space-2)', alignItems: 'center' }}
219
- >
220
- <TextInput
221
- aria-label={`${groupLabel} key ${String(index + 1)}`}
222
- placeholder="name"
223
- value={row.key}
224
- autoComplete="off"
225
- disabled={disabled}
226
- onChange={(event) => {
227
- const next = [...rows];
228
- next[index] = { ...row, key: event.target.value };
229
- onChange(next);
230
- }}
231
- />
232
- <TextInput
233
- aria-label={`${groupLabel} value ${String(index + 1)}`}
234
- placeholder="value"
235
- value={row.value}
236
- autoComplete="off"
237
- disabled={disabled}
238
- onChange={(event) => {
239
- const next = [...rows];
240
- next[index] = { ...row, value: event.target.value };
241
- onChange(next);
242
- }}
243
- />
244
- <Button
245
- type="button"
246
- variant="ghost"
247
- aria-label={`Remove ${groupLabel} ${String(index + 1)}`}
248
- disabled={disabled}
249
- onClick={() => {
250
- onChange(rows.filter((_, position) => position !== index));
251
- }}
252
- >
253
- <CloseIcon aria-hidden="true" />
254
- </Button>
255
- </div>
256
- ))}
257
- <div>
258
- <Button
259
- type="button"
260
- variant="ghost"
261
- disabled={disabled}
262
- onClick={() => {
263
- onChange([...rows, { key: '', value: '' }]);
264
- }}
265
- >
266
- Add parameter
267
- </Button>
268
- </div>
269
- </div>
270
- );
271
- }
272
-
273
- /** The initial mode: stored when the seed carries an `id`, inline otherwise. */
274
- function initialMode(value: TemplatedText | null): Mode {
275
- return value?.id !== undefined ? 'stored' : 'inline';
276
- }
277
-
278
118
  export function TemplatedTextField({
279
119
  label,
280
120
  value,
@@ -304,22 +144,7 @@ export function TemplatedTextField({
304
144
  nextStored: string,
305
145
  nextKwargs: readonly KwargRow[],
306
146
  ): void => {
307
- const kwargs = rowsToObject(nextKwargs);
308
- const kw = Object.keys(kwargs).length > 0 ? { kwargs } : {};
309
- if (nextMode === 'stored') {
310
- const id = nextStored.trim();
311
- if (id === '') {
312
- onChange(required ? { id: '', ...kw } : null);
313
- return;
314
- }
315
- onChange({ id, ...kw });
316
- return;
317
- }
318
- if (nextInline.trim() === '') {
319
- onChange(required ? { content: '', ...kw } : null);
320
- return;
321
- }
322
- onChange({ content: nextInline, ...kw });
147
+ onChange(templatedValueFrom(nextMode, nextInline, nextStored, nextKwargs, required));
323
148
  };
324
149
 
325
150
  const changeMode = (next: Mode): void => {
@@ -342,125 +167,33 @@ export function TemplatedTextField({
342
167
  emit(mode, inlineDraft, storedDraft, next);
343
168
  };
344
169
 
345
- const templateOptions = templates.map((template) => ({
346
- value: template.id,
347
- label: template.label ?? template.id,
348
- }));
349
- // A stored id the catalog does not offer cannot be picked from the list; it is
350
- // shown as an unresolved note rather than silently blanked.
351
- const unresolvedId =
352
- mode === 'stored' &&
353
- storedDraft.trim() !== '' &&
354
- !templatesLoading &&
355
- templatesError === undefined &&
356
- !templates.some((template) => template.id === storedDraft.trim())
357
- ? storedDraft.trim()
358
- : undefined;
359
-
360
- // The inline editor, shared by the present-storage inline mode and the
361
- // absent-storage inline-only rendering: the caller's rich editor when supplied,
362
- // else a plain textarea.
363
- const inlineEditor =
364
- renderInline !== undefined ? (
365
- renderInline({ label, value: inlineDraft, onChange: changeInline, error, hideLabel: true })
366
- ) : (
367
- <Field label={label} hideLabel error={error}>
368
- <Textarea
369
- value={inlineDraft}
370
- placeholder={placeholder}
371
- disabled={disabled}
372
- onChange={(event) => {
373
- changeInline(event.target.value);
374
- }}
375
- />
376
- </Field>
377
- );
378
-
379
- // A stored `id` already authored on the value: with storage absent it is shown
380
- // read-only rather than resolved, browsed, converted, or dropped.
381
- const authoredStoredId = value?.id;
382
- const showsReadOnlyStored = storageAbsent && authoredStoredId !== undefined;
383
-
384
- // The source region. While presence is unknown a placeholder stands in, so the
385
- // toggle is never shown and then removed. With storage absent the stored source
386
- // cannot work and is not offered: an inline or empty value edits inline; a value
387
- // carrying an `id` is shown read-only with an unavailability note. With storage
388
- // present the full inline/stored toggle and picker render.
389
- let source: ReactNode;
390
- if (storagePresenceLoading) {
391
- source = <Skeleton height={36} />;
392
- } else if (storageAbsent) {
393
- source = showsReadOnlyStored ? (
394
- <>
395
- {/* A labeled read-only reference, NOT an editable field: no input box, a
396
- "Stored template" caption, and the id in monospace — so it reads as a stored
397
- reference the way the state-template screen shows one, never as literal text a
398
- reader could mistake for the condition itself. A long id wraps rather than
399
- overflowing its row. */}
400
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-1)' }}>
401
- <span style={{ fontSize: 'var(--tai-text-sm)', color: 'var(--tai-color-text-muted)' }}>
402
- Stored template
403
- </span>
404
- <code style={{ fontFamily: 'var(--tai-font-mono)', wordBreak: 'break-all' }}>
405
- {authoredStoredId}
406
- </code>
407
- </div>
408
- <p
409
- style={{
410
- margin: 0,
411
- fontSize: 'var(--tai-text-sm)',
412
- color: 'var(--tai-color-text-muted)',
413
- }}
414
- >
415
- Stored templates are unavailable — this deployment has no storage backend.
416
- </p>
417
- </>
418
- ) : (
419
- inlineEditor
420
- );
421
- } else {
422
- source = (
423
- <>
424
- <RadioGroup
425
- aria-label={`${label} source`}
426
- variant="segmented"
427
- value={mode}
428
- disabled={disabled}
429
- options={[
430
- { value: 'inline', label: 'Inline text' },
431
- { value: 'stored', label: 'Stored template' },
432
- ]}
433
- onValueChange={(next) => {
434
- changeMode(next as Mode);
435
- }}
436
- />
437
-
438
- {mode === 'inline' ? (
439
- inlineEditor
440
- ) : templatesError !== undefined ? (
441
- <ErrorState message={templatesError} onRetry={onTemplatesRetry} />
442
- ) : templatesLoading ? (
443
- <Skeleton height={36} />
444
- ) : (
445
- <Field label={label} hideLabel error={error}>
446
- <Select
447
- placeholder={templates.length === 0 ? 'No templates available' : 'Select a template'}
448
- value={storedDraft}
449
- disabled={disabled || templates.length === 0}
450
- options={templateOptions}
451
- onValueChange={changeStored}
452
- />
453
- </Field>
454
- )}
455
-
456
- {unresolvedId !== undefined ? (
457
- <p role="alert" style={{ margin: 0, color: 'var(--tai-color-err-text)' }}>
458
- {resolveError ?? `The stored template "${unresolvedId}" is not available.`}
459
- </p>
460
- ) : null}
461
- </>
462
- );
463
- }
170
+ // A stored `id` already authored on the value is shown read-only when storage is
171
+ // absent — never resolved, browsed, converted, or dropped.
172
+ const showsReadOnlyStored = storageAbsent && value?.id !== undefined;
173
+
174
+ const source = (
175
+ <TemplatedTextSource
176
+ label={label}
177
+ mode={mode}
178
+ inlineDraft={inlineDraft}
179
+ storedDraft={storedDraft}
180
+ error={error}
181
+ disabled={disabled}
182
+ placeholder={placeholder}
183
+ renderInline={renderInline}
184
+ templates={templates}
185
+ templatesLoading={templatesLoading}
186
+ templatesError={templatesError}
187
+ onTemplatesRetry={onTemplatesRetry}
188
+ resolveError={resolveError}
189
+ storageAbsent={storageAbsent}
190
+ storagePresenceLoading={storagePresenceLoading}
191
+ authoredStoredId={value?.id}
192
+ onModeChange={changeMode}
193
+ onInlineChange={changeInline}
194
+ onStoredChange={changeStored}
195
+ />
196
+ );
464
197
 
465
198
  // ONE coherent group per field: the label + description form the group header, and
466
199
  // the source region and the render-parameters disclosure sit under it — so the
@@ -470,36 +203,11 @@ export function TemplatedTextField({
470
203
  // visually-hidden `<label for>` (see `hideLabel`). The container-name redundancy
471
204
  // `Field` accepts by design (a redundant container is audible, an unnamed group is
472
205
  // silent).
473
- //
474
- // A read-only stored value shows its render parameters as read-only text — the values
475
- // in normal weight, NOT the muted disabled inputs a reader could mistake for empty
476
- // placeholders — matching the read-only treatment of the id above; every editable
477
- // rendering keeps the full add/remove editor.
478
206
  return (
479
207
  <Field label={label} description={description} group>
480
208
  {source}
481
209
  {showsReadOnlyStored ? (
482
- kwargsRows.length > 0 ? (
483
- <div
484
- role="group"
485
- aria-label={`${label} render parameters`}
486
- style={{ display: 'flex', flexDirection: 'column', gap: 'var(--tai-space-1)' }}
487
- >
488
- <span style={{ fontSize: 'var(--tai-text-sm)', color: 'var(--tai-color-text-muted)' }}>
489
- Render parameters
490
- </span>
491
- {kwargsRows.map((row) => (
492
- <div
493
- key={row.key}
494
- style={{ fontFamily: 'var(--tai-font-mono)', wordBreak: 'break-all' }}
495
- >
496
- <code>{row.key}</code>
497
- <span style={{ color: 'var(--tai-color-text-muted)' }}>: </span>
498
- <code>{row.value}</code>
499
- </div>
500
- ))}
501
- </div>
502
- ) : null
210
+ <ReadOnlyKwargs label={label} rows={kwargsRows} />
503
211
  ) : (
504
212
  <KwargsEditor label={label} rows={kwargsRows} disabled={disabled} onChange={changeKwargs} />
505
213
  )}