@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
@@ -8,81 +8,25 @@
8
8
  * dir / tags / search in the URL or in local state; only the view mode persists
9
9
  * internally (per `viewSurface`).
10
10
  *
11
- * The tag glue lives here as exported helpers ({@link buildTagVocabulary},
11
+ * The tag glue lives in `explorer-tags` as exported helpers ({@link buildTagVocabulary},
12
12
  * {@link matchesSelectedTags}, {@link UNTAGGED_TOKEN}) so every consuming screen
13
13
  * shares one vocabulary/untagged-sentinel/OR-match rule rather than copying it.
14
14
  */
15
- import { useCallback, useEffect, useState, type ReactNode } from 'react';
15
+ import { useEffect, useState, type ReactNode } from 'react';
16
16
 
17
- import { EntityCardGrid } from './entity-card-grid';
18
- import { childFolders, FolderBreadcrumb, FolderRow, type Folder } from './folder-nav';
17
+ import { childFolders, FolderBreadcrumb, type Folder } from './folder-nav';
19
18
  import { SearchIcon } from './icons';
20
19
  import { TextInput } from './inputs';
21
20
  import { openTargetProps, type OpenTargetProps } from './open-target';
22
- import { Button, Card, EmptyState } from './primitives';
23
- import { ScrollRegion } from './scroll-region';
24
- import { Select } from './select';
25
- import { Table, TBody, TD, TH, THead, TR } from './table';
26
- import { useViewMode, ViewToggle } from './view-toggle';
27
-
28
- /** Reserved filter token for items carrying NO tag; namespaced so it never
29
- * collides with a real tag. */
30
- export const UNTAGGED_TOKEN = '__untagged__';
31
-
32
- export interface TagVocabularyEntry {
33
- readonly token: string;
34
- readonly label: string;
35
- readonly count: number;
36
- }
21
+ import { EmptyState } from './primitives';
22
+ import { useViewMode, ViewToggle, type ViewMode } from './view-toggle';
23
+ import { buildTagVocabulary, matchesSelectedTags } from './explorer-tags';
24
+ import { usePageSize } from './explorer-page-size';
25
+ import { TagFilterRow } from './explorer-tag-filter';
26
+ import { ExplorerBody, type ExplorerEntry } from './explorer-body';
37
27
 
38
- /** The tag vocabulary over `items`, name-sorted, plus an untagged pseudo-tag
39
- * (labelled `untaggedLabel`) when any item is untagged; a selected token whose
40
- * items vanished still appears (count 0) so it can be cleared. */
41
- export function buildTagVocabulary<T>(
42
- items: readonly T[],
43
- getTags: (item: T) => readonly string[],
44
- selectedTags: readonly string[],
45
- untaggedLabel: string,
46
- ): TagVocabularyEntry[] {
47
- const counts = new Map<string, number>();
48
- let untagged = 0;
49
- for (const item of items) {
50
- const tags = getTags(item);
51
- if (tags.length === 0) {
52
- untagged += 1;
53
- continue;
54
- }
55
- for (const tag of tags) counts.set(tag, (counts.get(tag) ?? 0) + 1);
56
- }
57
- const vocabulary: TagVocabularyEntry[] = [...counts.entries()]
58
- .sort(([a], [b]) => a.localeCompare(b))
59
- .map(([label, count]) => ({ token: label, label, count }));
60
- if (untagged > 0) {
61
- vocabulary.push({ token: UNTAGGED_TOKEN, label: untaggedLabel, count: untagged });
62
- }
63
- for (const token of selectedTags) {
64
- if (!vocabulary.some((entry) => entry.token === token)) {
65
- vocabulary.push({
66
- token,
67
- label: token === UNTAGGED_TOKEN ? untaggedLabel : token,
68
- count: 0,
69
- });
70
- }
71
- }
72
- return vocabulary;
73
- }
74
-
75
- /** OR semantics: an empty selection matches everything; otherwise an item
76
- * matches when it carries any selected tag, or is untagged and UNTAGGED_TOKEN
77
- * is selected. */
78
- export function matchesSelectedTags(
79
- itemTags: readonly string[],
80
- selected: ReadonlySet<string>,
81
- ): boolean {
82
- if (selected.size === 0) return true;
83
- if (itemTags.length === 0) return selected.has(UNTAGGED_TOKEN);
84
- return itemTags.some((tag) => selected.has(tag));
85
- }
28
+ export { UNTAGGED_TOKEN, buildTagVocabulary, matchesSelectedTags } from './explorer-tags';
29
+ export type { TagVocabularyEntry } from './explorer-tags';
86
30
 
87
31
  /** One header cell of the table view. `renderRow` must emit exactly this many
88
32
  * `<TD>`s per item; folder rows span all of them. */
@@ -172,140 +116,85 @@ export interface ExplorerViewProps<T> {
172
116
  readonly emptyStates: ExplorerEmptyStates;
173
117
  }
174
118
 
175
- /** A togglable tag chip; `aria-pressed` reflects whether the tag filters the list. */
176
- function TagChip({
177
- entry,
178
- active,
179
- onToggle,
180
- }: {
181
- readonly entry: TagVocabularyEntry;
182
- readonly active: boolean;
183
- readonly onToggle: () => void;
184
- }): ReactNode {
185
- return (
186
- <button
187
- type="button"
188
- className="tai-chip"
189
- aria-pressed={active}
190
- aria-label={`${entry.label} (${String(entry.count)})`}
191
- onClick={onToggle}
192
- >
193
- <span>{entry.label}</span>
194
- <span>{entry.count}</span>
195
- </button>
196
- );
197
- }
198
-
199
- /** The filter row: every selected chip plus as many unselected chips as fit under
200
- * the cap, with the remainder collapsed into a STATIC "+N more" count. An unset
201
- * cap shows every chip. */
202
- function TagFilterRow({
203
- vocabulary,
204
- selectedSet,
205
- onToggle,
206
- filterLabel,
207
- maxVisibleTags,
208
- }: {
209
- readonly vocabulary: readonly TagVocabularyEntry[];
210
- readonly selectedSet: ReadonlySet<string>;
211
- readonly onToggle: (token: string) => void;
212
- readonly filterLabel: string;
213
- readonly maxVisibleTags: number | undefined;
214
- }): ReactNode {
215
- const visible: TagVocabularyEntry[] = [];
216
- let hidden = 0;
217
- let unselectedShown = 0;
218
- for (const entry of vocabulary) {
219
- if (selectedSet.has(entry.token)) {
220
- visible.push(entry);
221
- } else if (maxVisibleTags === undefined || unselectedShown < maxVisibleTags) {
222
- visible.push(entry);
223
- unselectedShown += 1;
224
- } else {
225
- hidden += 1;
226
- }
227
- }
228
-
229
- return (
230
- <div role="group" aria-label={filterLabel} className="tai-row">
231
- {visible.map((entry) => (
232
- <TagChip
233
- key={entry.token}
234
- entry={entry}
235
- active={selectedSet.has(entry.token)}
236
- onToggle={() => {
237
- onToggle(entry.token);
238
- }}
239
- />
240
- ))}
241
- {hidden > 0 ? (
242
- <span className="tai-chip tai-chip-static">{`+${String(hidden)} more`}</span>
243
- ) : null}
244
- </div>
245
- );
246
- }
247
-
248
- /** A first-class folder entry — a `FolderRow` (icon + name, navigates inward)
249
- * beside its optional actions, in the layout shared by the row cell and the card. */
250
- function FolderEntry({
251
- folder,
252
- onNavigate,
253
- renderFolderActions,
119
+ /** The header row: the optional search field, the item count, and the
120
+ * list/card view toggle. */
121
+ function ExplorerControls<T>({
122
+ itemCount,
123
+ countLabel,
124
+ label,
125
+ search,
126
+ viewMode,
127
+ onViewModeChange,
254
128
  }: {
255
- readonly folder: Folder;
256
- readonly onNavigate: (folderId: string | null) => void;
257
- readonly renderFolderActions: ((folder: Folder) => ReactNode) | undefined;
129
+ readonly itemCount: number;
130
+ readonly countLabel: string;
131
+ readonly label: string;
132
+ readonly search: ExplorerSearch<T> | undefined;
133
+ readonly viewMode: ViewMode;
134
+ readonly onViewModeChange: (mode: ViewMode) => void;
258
135
  }): ReactNode {
259
136
  return (
260
- <div className="tai-explorer-folder">
261
- <FolderRow folder={folder} onOpen={onNavigate} />
262
- {renderFolderActions ? renderFolderActions(folder) : null}
137
+ <div className="tai-explorer-controls">
138
+ <div className="tai-row">
139
+ {search !== undefined ? (
140
+ // The magnifier is a leading icon INSIDE the input, not a detached
141
+ // glyph beside it: the wrapper positions the icon over the input's
142
+ // reserved leading padding.
143
+ <span className="tai-search-field">
144
+ <SearchIcon />
145
+ <TextInput
146
+ value={search.value}
147
+ aria-label={search.label}
148
+ placeholder={search.placeholder}
149
+ onChange={(event) => {
150
+ search.onChange(event.target.value);
151
+ }}
152
+ />
153
+ </span>
154
+ ) : null}
155
+ <span className="tai-muted">{`${String(itemCount)} ${countLabel}`}</span>
156
+ </div>
157
+ <ViewToggle value={viewMode} onValueChange={onViewModeChange} aria-label={`${label} view`} />
263
158
  </div>
264
159
  );
265
160
  }
266
161
 
267
- /** The page-size choices, and the default. */
268
- const PAGE_SIZES = [12, 24, 48, 96] as const;
269
- const DEFAULT_PAGE_SIZE = 24;
270
-
271
- const PAGE_SIZE_STORAGE_PREFIX = 'tai-studio.page-size.';
272
-
273
- function readStoredPageSize(surface: string): number {
274
- try {
275
- const raw = globalThis.localStorage.getItem(PAGE_SIZE_STORAGE_PREFIX + surface);
276
- const parsed = raw === null ? Number.NaN : Number(raw);
277
- if ((PAGE_SIZES as readonly number[]).includes(parsed)) return parsed;
278
- } catch {
279
- // No storage (private mode / non-browser) — fall back to the default.
280
- }
281
- return DEFAULT_PAGE_SIZE;
282
- }
283
-
284
162
  /**
285
- * The persisted page size for one `surface`, mirroring {@link useViewMode}: an
286
- * invalid or absent stored value degrades to the default, and a storage failure
287
- * degrades to in-memory only — never a thrown boot.
163
+ * The pointer-open props for one item's row/card, via the shared house pattern
164
+ * ({@link openTargetProps}): present only when `onOpenItem` is set, it yields to
165
+ * the item's own nested interactive elements (its name link, an actions kebab)
166
+ * and to a text-selection drag. The accessible activation path stays that nested
167
+ * name link, so no `keyboard` here — the row is never itself a focus stop.
288
168
  */
289
- function usePageSize(surface: string): readonly [number, (size: number) => void] {
290
- const [size, setSizeState] = useState<number>(() => readStoredPageSize(surface));
291
- const setSize = useCallback(
292
- (next: number) => {
293
- setSizeState(next);
294
- try {
295
- globalThis.localStorage.setItem(PAGE_SIZE_STORAGE_PREFIX + surface, String(next));
296
- } catch {
297
- // Storage unavailable — the choice still applies for this session.
298
- }
299
- },
300
- [surface],
301
- );
302
- return [size, setSize];
169
+ function itemOpenProps<T>(item: T, onOpenItem: ((item: T) => void) | undefined): OpenTargetProps {
170
+ return openTargetProps({
171
+ onOpen:
172
+ onOpenItem === undefined
173
+ ? undefined
174
+ : () => {
175
+ onOpenItem(item);
176
+ },
177
+ });
303
178
  }
304
179
 
305
- /** One page entry: a subfolder (sorted first) or a filtered item, in one array. */
306
- type ExplorerEntry<T> =
307
- | { readonly kind: 'folder'; readonly folder: Folder }
308
- | { readonly kind: 'item'; readonly item: T };
180
+ /** The current folder's filtered items and the tag vocabulary over the folder. */
181
+ function filterInFolder<T>(
182
+ inFolder: readonly T[],
183
+ tags: ExplorerTags<T> | undefined,
184
+ selectedSet: ReadonlySet<string> | undefined,
185
+ search: ExplorerSearch<T> | undefined,
186
+ query: string,
187
+ ): readonly T[] {
188
+ return inFolder.filter((item) => {
189
+ if (tags !== undefined && selectedSet !== undefined && selectedSet.size > 0) {
190
+ if (!matchesSelectedTags(tags.getTags(item), selectedSet)) return false;
191
+ }
192
+ if (search !== undefined && query !== '') {
193
+ if (!search.matches(item, query)) return false;
194
+ }
195
+ return true;
196
+ });
197
+ }
309
198
 
310
199
  export function ExplorerView<T>({
311
200
  items,
@@ -351,15 +240,7 @@ export function ExplorerView<T>({
351
240
  const inFolder = items.filter((item) => getFolderId(item) === currentFolderId);
352
241
 
353
242
  const selectedSet = tags !== undefined ? new Set(tags.selected) : undefined;
354
- const filtered = inFolder.filter((item) => {
355
- if (tags !== undefined && selectedSet !== undefined && selectedSet.size > 0) {
356
- if (!matchesSelectedTags(tags.getTags(item), selectedSet)) return false;
357
- }
358
- if (search !== undefined && query !== '') {
359
- if (!search.matches(item, query)) return false;
360
- }
361
- return true;
362
- });
243
+ const filtered = filterInFolder(inFolder, tags, selectedSet, search, query);
363
244
 
364
245
  const vocabulary =
365
246
  tags !== undefined
@@ -374,20 +255,7 @@ export function ExplorerView<T>({
374
255
  tags.onChange(next);
375
256
  };
376
257
 
377
- // The pointer-open props for one item's row/card, via the shared house pattern
378
- // ({@link openTargetProps}): present only when `onOpenItem` is set, it yields to
379
- // the item's own nested interactive elements (its name link, an actions kebab)
380
- // and to a text-selection drag. The accessible activation path stays that nested
381
- // name link, so no `keyboard` here — the row is never itself a focus stop.
382
- const openProps = (item: T): OpenTargetProps =>
383
- openTargetProps({
384
- onOpen:
385
- onOpenItem === undefined
386
- ? undefined
387
- : () => {
388
- onOpenItem(item);
389
- },
390
- });
258
+ const openProps = (item: T): OpenTargetProps => itemOpenProps(item, onOpenItem);
391
259
 
392
260
  const hasEntries = subfolders.length > 0 || filtered.length > 0;
393
261
 
@@ -406,136 +274,6 @@ export function ExplorerView<T>({
406
274
  const pageStart = (effectivePage - 1) * pageSize;
407
275
  const pageEntries = entries.slice(pageStart, pageStart + pageSize);
408
276
 
409
- const footer =
410
- entries.length > 0 ? (
411
- <nav className="tai-explorer-pagination" aria-label={`${label} pagination`}>
412
- {/* The pager counts ENTRIES — subfolders plus filtered items in the
413
- current directory — which is a different unit from the header's total
414
- item count across every folder. Naming the unit keeps the two honest:
415
- "1–4 of 4 entries" here versus "7 templates" above. */}
416
- <span className="tai-muted">{`${String(pageStart + 1)}–${String(Math.min(pageStart + pageSize, entries.length))} of ${String(entries.length)} entries`}</span>
417
- <div className="tai-row">
418
- <Select
419
- aria-label="Items per page"
420
- value={String(pageSize)}
421
- onValueChange={(value) => {
422
- setPageSize(Number(value));
423
- }}
424
- options={PAGE_SIZES.map((size) => ({
425
- value: String(size),
426
- label: `${String(size)} per page`,
427
- }))}
428
- />
429
- {/* aria-disabled, never the disabled attribute: a button removed from
430
- the tab order at the bound drops focus to the body (WCAG 2.4.3).
431
- The click guard makes activation at the bound an inert no-op. */}
432
- <Button
433
- variant="ghost"
434
- aria-label="Previous page"
435
- aria-disabled={effectivePage <= 1}
436
- onClick={() => {
437
- if (effectivePage <= 1) return;
438
- setPage(effectivePage - 1);
439
- }}
440
- >
441
- Prev
442
- </Button>
443
- <Button
444
- variant="ghost"
445
- aria-label="Next page"
446
- aria-disabled={effectivePage >= pageCount}
447
- onClick={() => {
448
- if (effectivePage >= pageCount) return;
449
- setPage(effectivePage + 1);
450
- }}
451
- >
452
- Next
453
- </Button>
454
- <span className="tai-muted">{`Page ${String(effectivePage)} of ${String(pageCount)}`}</span>
455
- </div>
456
- </nav>
457
- ) : null;
458
-
459
- let body: ReactNode;
460
- if (!hasEntries) {
461
- // The current folder renders nothing. A filter emptied it (`inFolder` non-empty)
462
- // → "no match"; otherwise the folder is genuinely empty. The ladder decides on
463
- // the full, unsliced sets, never on a page.
464
- body = (
465
- <EmptyState {...(inFolder.length > 0 ? emptyStates.noMatch : emptyStates.emptyFolder)} />
466
- );
467
- } else if (viewMode === 'cards') {
468
- body = (
469
- <>
470
- <EntityCardGrid aria-label={label}>
471
- {pageEntries.map((entry) =>
472
- entry.kind === 'folder' ? (
473
- <div role="listitem" key={`folder:${entry.folder.id}`}>
474
- <Card interactive>
475
- <FolderEntry
476
- folder={entry.folder}
477
- onNavigate={onNavigate}
478
- renderFolderActions={renderFolderActions}
479
- />
480
- </Card>
481
- </div>
482
- ) : (
483
- <div
484
- role="listitem"
485
- key={`item:${getItemKey(entry.item)}`}
486
- {...openProps(entry.item)}
487
- >
488
- {renderCard(entry.item)}
489
- </div>
490
- ),
491
- )}
492
- </EntityCardGrid>
493
- {footer}
494
- </>
495
- );
496
- } else {
497
- // Folder rows share the item table, spanning every column, sorted above items —
498
- // so a folder-with-only-subfolders is folder ROWS, never a header-only table.
499
- // A too-wide table scrolls inside its own box rather than widening the page.
500
- body = (
501
- <>
502
- <ScrollRegion label={label}>
503
- <Table>
504
- <THead>
505
- <TR>
506
- {columns.map((column) => (
507
- <TH key={column.key} numeric={column.numeric}>
508
- {column.header}
509
- </TH>
510
- ))}
511
- </TR>
512
- </THead>
513
- <TBody>
514
- {pageEntries.map((entry) =>
515
- entry.kind === 'folder' ? (
516
- <TR key={`folder:${entry.folder.id}`}>
517
- <TD colSpan={columns.length}>
518
- <FolderEntry
519
- folder={entry.folder}
520
- onNavigate={onNavigate}
521
- renderFolderActions={renderFolderActions}
522
- />
523
- </TD>
524
- </TR>
525
- ) : (
526
- <TR key={`item:${getItemKey(entry.item)}`} {...openProps(entry.item)}>
527
- {renderRow(entry.item)}
528
- </TR>
529
- ),
530
- )}
531
- </TBody>
532
- </Table>
533
- </ScrollRegion>
534
- {footer}
535
- </>
536
- );
537
- }
538
-
539
277
  // Every surface label is a regular English plural (Tools/Templates/Resources/
540
278
  // Flows), so a count of one drops the trailing "s".
541
279
  const countLabel =
@@ -543,28 +281,14 @@ export function ExplorerView<T>({
543
281
 
544
282
  return (
545
283
  <div className="tai-explorer">
546
- <div className="tai-explorer-controls">
547
- <div className="tai-row">
548
- {search !== undefined ? (
549
- // The magnifier is a leading icon INSIDE the input, not a detached
550
- // glyph beside it: the wrapper positions the icon over the input's
551
- // reserved leading padding.
552
- <span className="tai-search-field">
553
- <SearchIcon />
554
- <TextInput
555
- value={search.value}
556
- aria-label={search.label}
557
- placeholder={search.placeholder}
558
- onChange={(event) => {
559
- search.onChange(event.target.value);
560
- }}
561
- />
562
- </span>
563
- ) : null}
564
- <span className="tai-muted">{`${String(items.length)} ${countLabel}`}</span>
565
- </div>
566
- <ViewToggle value={viewMode} onValueChange={setViewMode} aria-label={`${label} view`} />
567
- </div>
284
+ <ExplorerControls
285
+ itemCount={items.length}
286
+ countLabel={countLabel}
287
+ label={label}
288
+ search={search}
289
+ viewMode={viewMode}
290
+ onViewModeChange={setViewMode}
291
+ />
568
292
 
569
293
  <FolderBreadcrumb
570
294
  folders={folders}
@@ -583,7 +307,31 @@ export function ExplorerView<T>({
583
307
  />
584
308
  ) : null}
585
309
 
586
- {body}
310
+ <ExplorerBody
311
+ hasEntries={hasEntries}
312
+ inFolderCount={inFolder.length}
313
+ emptyStates={emptyStates}
314
+ viewMode={viewMode}
315
+ label={label}
316
+ columns={columns}
317
+ pageEntries={pageEntries}
318
+ getItemKey={getItemKey}
319
+ renderRow={renderRow}
320
+ renderCard={renderCard}
321
+ openProps={openProps}
322
+ onNavigate={onNavigate}
323
+ renderFolderActions={renderFolderActions}
324
+ pager={{
325
+ label,
326
+ pageStart,
327
+ pageSize,
328
+ total: entries.length,
329
+ effectivePage,
330
+ pageCount,
331
+ onPageChange: setPage,
332
+ onPageSizeChange: setPageSize,
333
+ }}
334
+ />
587
335
  </div>
588
336
  );
589
337
  }