basalt-ui 1.10.0 → 1.12.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 (273) hide show
  1. package/AGENTS.md +21 -21
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +36 -27
  4. package/agent/rules/basalt-content.md +16 -3
  5. package/dist/agent/adapter.d.ts +146 -0
  6. package/dist/agent/adapter.d.ts.map +1 -0
  7. package/dist/agent/adapter.js +473 -0
  8. package/dist/agent/adapter.test.d.ts +2 -0
  9. package/dist/agent/adapter.test.d.ts.map +1 -0
  10. package/dist/agent/ai-sdk-transport.d.ts +3 -52
  11. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  12. package/dist/agent/ai-sdk-transport.js +108 -24
  13. package/dist/agent/ai-sdk-transport.test.d.ts +2 -0
  14. package/dist/agent/ai-sdk-transport.test.d.ts.map +1 -0
  15. package/dist/agent/coalesce.d.ts +42 -0
  16. package/dist/agent/coalesce.d.ts.map +1 -0
  17. package/dist/agent/coalesce.js +137 -0
  18. package/dist/agent/coalesce.test.d.ts +2 -0
  19. package/dist/agent/coalesce.test.d.ts.map +1 -0
  20. package/dist/agent/foreign.d.ts +97 -0
  21. package/dist/agent/foreign.d.ts.map +1 -0
  22. package/dist/agent/foreign.js +18 -0
  23. package/dist/agent/foreign.test.d.ts +2 -0
  24. package/dist/agent/foreign.test.d.ts.map +1 -0
  25. package/dist/agent/history.d.ts +2 -0
  26. package/dist/agent/history.d.ts.map +1 -1
  27. package/dist/agent/id.d.ts +65 -0
  28. package/dist/agent/id.d.ts.map +1 -0
  29. package/dist/agent/id.js +30 -0
  30. package/dist/agent/id.test.d.ts +2 -0
  31. package/dist/agent/id.test.d.ts.map +1 -0
  32. package/dist/agent/index.d.ts +11 -4
  33. package/dist/agent/index.d.ts.map +1 -1
  34. package/dist/agent/index.js +18 -3
  35. package/dist/agent/merge.d.ts +8 -0
  36. package/dist/agent/merge.d.ts.map +1 -0
  37. package/dist/agent/merge.js +35 -0
  38. package/dist/agent/merge.test.d.ts +2 -0
  39. package/dist/agent/merge.test.d.ts.map +1 -0
  40. package/dist/agent/part-list.d.ts +49 -34
  41. package/dist/agent/part-list.d.ts.map +1 -1
  42. package/dist/agent/part-list.js +53 -7
  43. package/dist/agent/part-list.test.d.ts +2 -0
  44. package/dist/agent/part-list.test.d.ts.map +1 -0
  45. package/dist/agent/parts.d.ts +135 -20
  46. package/dist/agent/parts.d.ts.map +1 -1
  47. package/dist/agent/parts.js +138 -11
  48. package/dist/agent/parts.test.d.ts +2 -0
  49. package/dist/agent/parts.test.d.ts.map +1 -0
  50. package/dist/agent/thread.d.ts +14 -0
  51. package/dist/agent/thread.d.ts.map +1 -1
  52. package/dist/agent/thread.js +8 -2
  53. package/dist/agent/thread.test.d.ts +2 -0
  54. package/dist/agent/thread.test.d.ts.map +1 -0
  55. package/dist/agent/transport.d.ts +39 -3
  56. package/dist/agent/transport.d.ts.map +1 -1
  57. package/dist/agent/transport.js +20 -17
  58. package/dist/agent/transport.test.d.ts +2 -0
  59. package/dist/agent/transport.test.d.ts.map +1 -0
  60. package/dist/agent/use-agent-stream.d.ts +2 -1
  61. package/dist/agent/use-agent-stream.d.ts.map +1 -1
  62. package/dist/agent/use-agent-stream.js +5 -2
  63. package/dist/agent/use-agent-stream.test.d.ts +2 -0
  64. package/dist/agent/use-agent-stream.test.d.ts.map +1 -0
  65. package/dist/agent/use-agent-thread-runs.d.ts +9 -3
  66. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  67. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +2 -0
  68. package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +1 -0
  69. package/dist/agent/use-agent-thread-runs.js +75 -23
  70. package/dist/agent/use-agent-thread-runs.resume.test.d.ts +2 -0
  71. package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +1 -0
  72. package/dist/agent/use-agent-thread-runs.test.d.ts +2 -0
  73. package/dist/agent/use-agent-thread-runs.test.d.ts.map +1 -0
  74. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +2 -0
  75. package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +1 -0
  76. package/dist/agent-chat/composer.d.ts +130 -8
  77. package/dist/agent-chat/composer.d.ts.map +1 -1
  78. package/dist/agent-chat/composer.js +189 -23
  79. package/dist/agent-chat/composer.test.d.ts +2 -0
  80. package/dist/agent-chat/composer.test.d.ts.map +1 -0
  81. package/dist/agent-chat/index.d.ts +3 -1
  82. package/dist/agent-chat/index.d.ts.map +1 -1
  83. package/dist/agent-chat/index.js +2 -0
  84. package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
  85. package/dist/agent-chat/thread-detail-panel.js +1 -1
  86. package/dist/agent-chat/thread-message.d.ts +24 -7
  87. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  88. package/dist/agent-chat/thread-message.js +115 -45
  89. package/dist/agent-chat/thread-message.test.d.ts +2 -0
  90. package/dist/agent-chat/thread-message.test.d.ts.map +1 -0
  91. package/dist/agent-chat/thread-workspace.js +1 -1
  92. package/dist/agent-chat/tool-chip.d.ts +21 -0
  93. package/dist/agent-chat/tool-chip.d.ts.map +1 -0
  94. package/dist/agent-chat/tool-chip.js +120 -0
  95. package/dist/agent-chat/tool-chip.test.d.ts +2 -0
  96. package/dist/agent-chat/tool-chip.test.d.ts.map +1 -0
  97. package/dist/charts/primitives/ChartFrame.test.d.ts +2 -0
  98. package/dist/charts/primitives/ChartFrame.test.d.ts.map +1 -0
  99. package/dist/charts/utils/domain.test.d.ts +2 -0
  100. package/dist/charts/utils/domain.test.d.ts.map +1 -0
  101. package/dist/cli/check-theme.test.d.ts +2 -0
  102. package/dist/cli/check-theme.test.d.ts.map +1 -0
  103. package/dist/cli/help.test.d.ts +2 -0
  104. package/dist/cli/help.test.d.ts.map +1 -0
  105. package/dist/cli/index.d.ts.map +1 -1
  106. package/dist/cli/init-sync.test.d.ts +2 -0
  107. package/dist/cli/init-sync.test.d.ts.map +1 -0
  108. package/dist/cli/placement-engine.test.d.ts +2 -0
  109. package/dist/cli/placement-engine.test.d.ts.map +1 -0
  110. package/dist/cli/tarball-parity.test.d.ts +2 -0
  111. package/dist/cli/tarball-parity.test.d.ts.map +1 -0
  112. package/dist/cli/template-format.test.d.ts +2 -0
  113. package/dist/cli/template-format.test.d.ts.map +1 -0
  114. package/dist/cli/template-ledger.test.d.ts +2 -0
  115. package/dist/cli/template-ledger.test.d.ts.map +1 -0
  116. package/dist/cli/tokens-css.test.d.ts +2 -0
  117. package/dist/cli/tokens-css.test.d.ts.map +1 -0
  118. package/dist/content/article-actions.test.d.ts +2 -0
  119. package/dist/content/article-actions.test.d.ts.map +1 -0
  120. package/dist/content/article-model.test.d.ts +2 -0
  121. package/dist/content/article-model.test.d.ts.map +1 -0
  122. package/dist/content/block-split.test.d.ts +2 -0
  123. package/dist/content/block-split.test.d.ts.map +1 -0
  124. package/dist/content/fence-block.d.ts +37 -10
  125. package/dist/content/fence-block.d.ts.map +1 -1
  126. package/dist/content/fence-block.js +80 -14
  127. package/dist/content/heading-components.d.ts.map +1 -1
  128. package/dist/content/heading-components.js +7 -4
  129. package/dist/content/heading-components.test.d.ts +2 -0
  130. package/dist/content/heading-components.test.d.ts.map +1 -0
  131. package/dist/content/index.d.ts +6 -3
  132. package/dist/content/index.d.ts.map +1 -1
  133. package/dist/content/index.js +5 -1
  134. package/dist/content/markdown.d.ts +101 -14
  135. package/dist/content/markdown.d.ts.map +1 -1
  136. package/dist/content/markdown.js +141 -28
  137. package/dist/content/markdown.test.d.ts +2 -0
  138. package/dist/content/markdown.test.d.ts.map +1 -0
  139. package/dist/content/prose.module.css.test.d.ts +2 -0
  140. package/dist/content/prose.module.css.test.d.ts.map +1 -0
  141. package/dist/content/sanitize.d.ts +170 -0
  142. package/dist/content/sanitize.d.ts.map +1 -0
  143. package/dist/content/sanitize.js +178 -0
  144. package/dist/content/sanitize.test.d.ts +2 -0
  145. package/dist/content/sanitize.test.d.ts.map +1 -0
  146. package/dist/content/slug.test.d.ts +2 -0
  147. package/dist/content/slug.test.d.ts.map +1 -0
  148. package/dist/dashboard/stat-card.test.d.ts +2 -0
  149. package/dist/dashboard/stat-card.test.d.ts.map +1 -0
  150. package/dist/guard/check-source.test.d.ts +2 -0
  151. package/dist/guard/check-source.test.d.ts.map +1 -0
  152. package/dist/guard/guard-hook.test.d.ts +2 -0
  153. package/dist/guard/guard-hook.test.d.ts.map +1 -0
  154. package/dist/guard/index.d.ts.map +1 -1
  155. package/dist/guard/index.js +1 -3
  156. package/dist/index.d.ts +4 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +12 -2
  159. package/dist/provider/index.test.d.ts +2 -0
  160. package/dist/provider/index.test.d.ts.map +1 -0
  161. package/dist/provider/lab-theme.test.d.ts +2 -0
  162. package/dist/provider/lab-theme.test.d.ts.map +1 -0
  163. package/dist/router-tanstack/multi-search-param-store.test.d.ts +2 -0
  164. package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +1 -0
  165. package/dist/styles.floor.test.d.ts +2 -0
  166. package/dist/styles.floor.test.d.ts.map +1 -0
  167. package/dist/surfaces.d.ts +5 -5
  168. package/dist/surfaces.d.ts.map +1 -1
  169. package/dist/surfaces.js +13 -4
  170. package/dist/theme/border-coverage.test.d.ts +18 -0
  171. package/dist/theme/border-coverage.test.d.ts.map +1 -0
  172. package/dist/theme/contrast.test.d.ts +2 -0
  173. package/dist/theme/contrast.test.d.ts.map +1 -0
  174. package/dist/theme/density-option.test.d.ts +2 -0
  175. package/dist/theme/density-option.test.d.ts.map +1 -0
  176. package/dist/theme/density-relations.test.d.ts +2 -0
  177. package/dist/theme/density-relations.test.d.ts.map +1 -0
  178. package/dist/theme/derive-theme.test.d.ts +2 -0
  179. package/dist/theme/derive-theme.test.d.ts.map +1 -0
  180. package/dist/theme/fonts-option.test.d.ts +2 -0
  181. package/dist/theme/fonts-option.test.d.ts.map +1 -0
  182. package/dist/theme/radius-option.test.d.ts +2 -0
  183. package/dist/theme/radius-option.test.d.ts.map +1 -0
  184. package/dist/theme/radius.test.d.ts +2 -0
  185. package/dist/theme/radius.test.d.ts.map +1 -0
  186. package/dist/theme/shadow-surfaces.test.d.ts +26 -0
  187. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -0
  188. package/dist/theme/spacing.test.d.ts +2 -0
  189. package/dist/theme/spacing.test.d.ts.map +1 -0
  190. package/dist/theme/use-basalt-spacing.test.d.ts +2 -0
  191. package/dist/theme/use-basalt-spacing.test.d.ts.map +1 -0
  192. package/dist/theme-lab/derive-controls.test.d.ts +2 -0
  193. package/dist/theme-lab/derive-controls.test.d.ts.map +1 -0
  194. package/dist/theme-lab/derive-state.test.d.ts +2 -0
  195. package/dist/theme-lab/derive-state.test.d.ts.map +1 -0
  196. package/dist/theme-lab/theme-lab.test.d.ts +2 -0
  197. package/dist/theme-lab/theme-lab.test.d.ts.map +1 -0
  198. package/dist/tokens/build-fonts-css.test.d.ts +2 -0
  199. package/dist/tokens/build-fonts-css.test.d.ts.map +1 -0
  200. package/dist/tokens/build-radius-css.test.d.ts +2 -0
  201. package/dist/tokens/build-radius-css.test.d.ts.map +1 -0
  202. package/dist/tokens/density.test.d.ts +2 -0
  203. package/dist/tokens/density.test.d.ts.map +1 -0
  204. package/dist/tokens/derive-radius.test.d.ts +2 -0
  205. package/dist/tokens/derive-radius.test.d.ts.map +1 -0
  206. package/dist/tokens/derive.test.d.ts +2 -0
  207. package/dist/tokens/derive.test.d.ts.map +1 -0
  208. package/dist/tokens/palette.test.d.ts +2 -0
  209. package/dist/tokens/palette.test.d.ts.map +1 -0
  210. package/dist/vite-color-mix.test.d.ts +2 -0
  211. package/dist/vite-color-mix.test.d.ts.map +1 -0
  212. package/dist/vite.test.d.ts +2 -0
  213. package/dist/vite.test.d.ts.map +1 -0
  214. package/llms.txt +7 -7
  215. package/package.json +6 -1
  216. package/src/agent/adapter.test.ts +1403 -0
  217. package/src/agent/adapter.ts +887 -0
  218. package/src/agent/ai-sdk-transport.test.ts +380 -55
  219. package/src/agent/ai-sdk-transport.ts +206 -53
  220. package/src/agent/coalesce.test.ts +387 -0
  221. package/src/agent/coalesce.ts +229 -0
  222. package/src/agent/foreign.test.ts +51 -0
  223. package/src/agent/foreign.ts +140 -0
  224. package/src/agent/history.ts +2 -0
  225. package/src/agent/id.test.ts +75 -0
  226. package/src/agent/id.ts +91 -0
  227. package/src/agent/index.ts +34 -3
  228. package/src/agent/merge.test.ts +206 -0
  229. package/src/agent/merge.ts +114 -0
  230. package/src/agent/part-list.test.tsx +112 -0
  231. package/src/agent/part-list.tsx +108 -35
  232. package/src/agent/parts.test.ts +516 -57
  233. package/src/agent/parts.ts +323 -32
  234. package/src/agent/thread.test.ts +75 -2
  235. package/src/agent/thread.ts +21 -1
  236. package/src/agent/transport.test.ts +70 -0
  237. package/src/agent/transport.ts +72 -20
  238. package/src/agent/use-agent-stream.test.tsx +53 -19
  239. package/src/agent/use-agent-stream.ts +13 -3
  240. package/src/agent/use-agent-thread-runs.foreign.test.tsx +138 -0
  241. package/src/agent/use-agent-thread-runs.resume.test.tsx +44 -11
  242. package/src/agent/use-agent-thread-runs.test.tsx +418 -22
  243. package/src/agent/use-agent-thread-runs.ts +170 -26
  244. package/src/agent/use-agent-thread-runs.wedge.test.tsx +15 -7
  245. package/src/agent-chat/composer.test.tsx +924 -0
  246. package/src/agent-chat/composer.tsx +521 -52
  247. package/src/agent-chat/index.ts +5 -1
  248. package/src/agent-chat/thread-detail-panel.tsx +3 -1
  249. package/src/agent-chat/thread-message.test.tsx +351 -0
  250. package/src/agent-chat/thread-message.tsx +262 -64
  251. package/src/agent-chat/thread-workspace.tsx +1 -1
  252. package/src/agent-chat/tool-chip.test.tsx +139 -0
  253. package/src/agent-chat/tool-chip.tsx +219 -0
  254. package/src/cli/index.ts +0 -1
  255. package/src/cli/template-format.test.ts +2 -2
  256. package/src/content/fence-block.tsx +242 -15
  257. package/src/content/heading-components.test.tsx +65 -0
  258. package/src/content/heading-components.tsx +26 -5
  259. package/src/content/index.ts +19 -3
  260. package/src/content/markdown.test.tsx +603 -0
  261. package/src/content/markdown.tsx +355 -35
  262. package/src/content/sanitize.test.ts +659 -0
  263. package/src/content/sanitize.ts +510 -0
  264. package/src/dashboard/stat-card.test.tsx +6 -1
  265. package/src/guard/check-source.test.ts +8 -4
  266. package/src/guard/index.ts +1 -3
  267. package/src/index.ts +15 -1
  268. package/src/surfaces.ts +13 -4
  269. package/src/theme/contrast.test.ts +1 -4
  270. package/src/theme/density-relations.test.ts +4 -2
  271. package/src/theme/fonts-option.test.ts +7 -1
  272. package/src/theme/shadow-surfaces.test.ts +12 -6
  273. package/src/theme/use-basalt-spacing.test.tsx +2 -1
@@ -0,0 +1,510 @@
1
+ /**
2
+ * Sanitize-schema composition for `Markdown` (docs/AGENT-CHAT-SPEC.md §7). Owns three things: the
3
+ * additions-only extension type a consumer hands to `Markdown`'s `sanitizeSchema` prop, basalt's
4
+ * own additions layer, and the additive deep merge that composes both over `rehype-sanitize`'s
5
+ * `defaultSchema`.
6
+ *
7
+ * WHY A DATA EXTENSION, NOT A `(base) => Schema` CALLBACK: a callback can return `{}` and silently
8
+ * disable sanitization, and a "never replace" comment is not enforcement. `SanitizeSchemaExtension`
9
+ * makes removal UNREPRESENTABLE — there is no code path in `mergeSanitizeSchema` that shrinks the
10
+ * base. That, plus `./markdown` appending its sanitize pass AFTER the consumer's `rehypePlugins`,
11
+ * is what keeps the baseline un-defeatable from outside the package.
12
+ *
13
+ * The `Schema` shape is MIRRORED here rather than imported from `hast-util-sanitize`, so the
14
+ * content layer's types never depend on an optional peer. `rehype-sanitize` is dynamically imported
15
+ * by `./markdown`, which composes the effective schema at load time:
16
+ *
17
+ * mergeSanitizeSchema(defaultSchema, BASALT_SANITIZE_SCHEMA, props.sanitizeSchema)
18
+ *
19
+ * THE TRAP THIS MODULE EXISTS TO AVOID: `hast-util-sanitize` merges a supplied schema with a
20
+ * shallow top-level spread (`{...defaultSchema, ...options}`). Passing an `attributes` object
21
+ * therefore REPLACES every default attribute allowance rather than adding to it — one consumer
22
+ * addition would silently strip `href` from links, `className` from code fences, and the whole
23
+ * `'*'` fallback. Upstream's answer is the third-party `deepmerge` package; basalt owns the small
24
+ * merge below instead of taking the dependency.
25
+ *
26
+ * "ADDITIONS ONLY" IS A STATEMENT ABOUT THE SANITIZER'S BEHAVIOUR, NOT ABOUT THE SCHEMA'S SHAPE.
27
+ * A merge that only ever grows arrays still removes allowances, because of three things
28
+ * `hast-util-sanitize@5` does when it CONSUMES the schema (`lib/index.js`, verified against the
29
+ * installed copy). Each is load-bearing for the code below:
30
+ *
31
+ * 1. `clobberPrefix` is gated on TRUTHINESS — `schema.clobber && schema.clobberPrefix &&
32
+ * schema.clobber.includes(key)`. ANY falsy prefix (`''`, but equally `null`, `0`, `false`,
33
+ * `NaN` — whatever an untyped caller or a cast smuggles through) therefore turns clobber
34
+ * protection OFF entirely and emits `id`/`name`/`ariaLabelledBy`/`ariaDescribedBy` unprefixed,
35
+ * which is the DOM-clobbering hole `user-content-` exists to close. See `adoptClobberPrefix`.
36
+ * 2. The `'*'` fallback is consulted ONLY when the tag-specific lookup yields `null`/`undefined`.
37
+ * For an ARRAY-valued property (`className`, `headers`, …) a matching tag-specific entry
38
+ * returns `[]` — neither — so the fallback is skipped. Introducing a tag-specific entry for a
39
+ * property the base only allowed via `'*'` therefore DELETES that allowance. See
40
+ * `rescueWildcardDefinitions`.
41
+ * 3. `findDefinition` returns the FIRST entry whose name matches, so any later entry for the same
42
+ * property name is dead code (it also treats a `'data*'` entry as the fallback for any
43
+ * `data`-prefixed key). Concatenating a consumer's entry after the base's is a silent no-op.
44
+ * See `unionDefinitions`, which collapses to ONE entry per property name.
45
+ */
46
+
47
+ // ── Schema shape (structural mirror of hast-util-sanitize's `Schema`) ─────────────────────────
48
+
49
+ /**
50
+ * One entry in an `attributes` allow-list. A bare string allows any value for that property; the
51
+ * tuple form allow-lists specific values (a `RegExp` matches a value, e.g. `['className',
52
+ * /^language-./]`). A ONE-element tuple (`['className']`) also allows any value — upstream only
53
+ * treats an entry as an allow-list when `definition.length > 1`.
54
+ *
55
+ * The `null`/`undefined` arms mirror upstream's `PropertyDefinition` exactly (its rest element is
56
+ * `Exclude<Properties[keyof Properties], Array<any>> | RegExp`, which includes both). They are
57
+ * inert: a sanitized value is always a boolean, number or string, so `allowed === value` can never
58
+ * match one.
59
+ */
60
+ export type SanitizePropertyDefinition =
61
+ | string
62
+ | readonly [string, ...ReadonlyArray<boolean | number | string | RegExp | null | undefined>]
63
+
64
+ /**
65
+ * Structural mirror of `hast-util-sanitize`'s `Schema`, defined here rather than imported so the
66
+ * content layer never depends on an OPTIONAL peer's types. This is the OUTPUT shape: no `null`
67
+ * arms, so nothing downstream has to re-check a field the merge already normalized away.
68
+ */
69
+ export type SanitizeSchema = {
70
+ readonly allowComments?: boolean
71
+ readonly allowDoctypes?: boolean
72
+ readonly ancestors?: Readonly<Record<string, readonly string[]>>
73
+ readonly attributes?: Readonly<Record<string, readonly SanitizePropertyDefinition[]>>
74
+ readonly clobber?: readonly string[]
75
+ readonly clobberPrefix?: string
76
+ readonly protocols?: Readonly<Record<string, readonly string[]>>
77
+ readonly required?: Readonly<Record<string, Readonly<Record<string, unknown>>>>
78
+ readonly strip?: readonly string[]
79
+ readonly tagNames?: readonly string[]
80
+ }
81
+ /**
82
+ * INPUT shape for `mergeSanitizeSchema`'s base — `SanitizeSchema` with upstream's `| null |
83
+ * undefined` on every field (and on every `protocols` value), so `rehype-sanitize`'s own
84
+ * `defaultSchema` assigns to it directly with NO cast at the call site.
85
+ *
86
+ * It is deliberately separate from `SanitizeSchema` rather than a widening of it: the null arms
87
+ * belong to what callers may HAND IN, not to what the merge HANDS BACK. `normalizeSchemaInput`
88
+ * drops them, so no `null` ever reaches the exported output type.
89
+ */
90
+ export type SanitizeSchemaInput = {
91
+ readonly allowComments?: boolean | null | undefined
92
+ readonly allowDoctypes?: boolean | null | undefined
93
+ readonly ancestors?: Readonly<Record<string, readonly string[]>> | null | undefined
94
+ readonly attributes?:
95
+ | Readonly<Record<string, readonly SanitizePropertyDefinition[]>>
96
+ | null
97
+ | undefined
98
+ readonly clobber?: readonly string[] | null | undefined
99
+ readonly clobberPrefix?: string | null | undefined
100
+ readonly protocols?:
101
+ | Readonly<Record<string, readonly string[] | null | undefined>>
102
+ | null
103
+ | undefined
104
+ readonly required?: Readonly<Record<string, Readonly<Record<string, unknown>>>> | null | undefined
105
+ readonly strip?: readonly string[] | null | undefined
106
+ readonly tagNames?: readonly string[] | null | undefined
107
+ }
108
+
109
+ /**
110
+ * ADDITIONS ONLY — the four fields it is safe to widen from outside the package. Removal is
111
+ * deliberately not expressible: there is no way to shrink `tagNames`, drop an attribute allowance,
112
+ * narrow `protocols`, or clear `strip`/`clobber`/`ancestors`/`required`.
113
+ *
114
+ * `clobberPrefix` is the one replace-valued field. Upstream's actual gate is `schema.clobberPrefix`
115
+ * read for TRUTHINESS (`lib/index.js`), not `=== ''` — so `null`, `0`, `false`, `NaN`, and `''` are
116
+ * ALL disarming values, and only a genuine non-empty string keeps the guard live. TypeScript cannot
117
+ * express "a non-empty string" on a plain data field (and the declared `string` type is a
118
+ * compile-time claim a JS caller or an `as` cast can defeat), so `adoptClobberPrefix` re-checks
119
+ * `typeof` at runtime and treats anything that is not a non-empty string as a disarm attempt —
120
+ * ignored with a dev warning, never applied.
121
+ */
122
+ export type SanitizeSchemaExtension = {
123
+ readonly tagNames?: readonly string[]
124
+ readonly attributes?: Readonly<Record<string, readonly SanitizePropertyDefinition[]>>
125
+ readonly protocols?: Readonly<Record<string, readonly string[]>>
126
+ readonly clobberPrefix?: string
127
+ }
128
+
129
+ // ── basalt's own additions layer ──────────────────────────────────────────────────────────────
130
+
131
+ /**
132
+ * basalt's own element allowances, layered over `rehype-sanitize`'s `defaultSchema`.
133
+ *
134
+ * THIS IS THE ADDITIONS LAYER, NOT THE FULL BASELINE. It is deliberately NOT a materialized
135
+ * `defaultSchema + basalt` schema: `defaultSchema` lives in `rehype-sanitize`, an OPTIONAL peer, so
136
+ * materializing it at module scope would require a static import that hard-requires the peer for
137
+ * every consumer of `basalt-ui/content` — and vendoring a copy would silently drift from upstream's
138
+ * GitHub-grade baseline, which for a security default is worse. The effective schema is composed
139
+ * where the peer is dynamically imported (see the module JSDoc).
140
+ *
141
+ * IT IS EMPTY, AND THAT IS THE HONEST ANSWER — everything basalt's markdown pipeline emits into the
142
+ * hast tree is already permitted by `defaultSchema`, verified entry by entry:
143
+ * - fenced code — `pre` > `code` with a `language-*` class: `defaultSchema.attributes.code` is
144
+ * `[['className', /^language-./]]`, which covers `language-mermaid` too. The fence's title comes
145
+ * from the `code` node's `data.meta`, and `data` is preserved by the sanitizer verbatim.
146
+ * - headings — plain `h1`..`h6`; the slug `id` and the hover anchor are added by basalt's React
147
+ * renderers AFTER sanitization, so they never pass through the schema.
148
+ * - GFM alerts — an ordinary `blockquote` whose first paragraph carries a `[!NOTE]`-style text
149
+ * marker; `Callout` is chosen in the renderer, again downstream of the hast tree.
150
+ * - GFM tables / task lists / footnotes / strikethrough — `table`+`align`, `input[type=checkbox]
151
+ * [disabled]`, `.task-list-item`, `section[data-footnotes]`, `del` are all explicit
152
+ * `defaultSchema` allowances.
153
+ * - links/images — `href`/`src` survive the schema and are then narrowed further by
154
+ * `./url-hardening`'s prefix allowlist.
155
+ *
156
+ * So: do not "fill this in". Every entry added here widens the security baseline for every consumer
157
+ * and must be justified by output basalt actually emits. `style`, event handlers, and new protocols
158
+ * are out of bounds; an app-specific need belongs in that app's `sanitizeSchema` prop.
159
+ */
160
+ export const BASALT_SANITIZE_SCHEMA: SanitizeSchemaExtension = {}
161
+
162
+ // ── Input normalization ───────────────────────────────────────────────────────────────────────
163
+
164
+ /** The `attributes` key upstream treats as "applies to every element". */
165
+ const WILDCARD = '*'
166
+
167
+ /**
168
+ * Drops the `null`/`undefined` arms `SanitizeSchemaInput` carries so the merge — and everything it
169
+ * returns — works in the non-null `SanitizeSchema` world. A `null` `protocols` VALUE is dropped
170
+ * with its key: upstream reads a missing key and a `null` one identically ("no protocols defined?
171
+ * then everything is fine"), so this is shape-only.
172
+ */
173
+ function isPresent<T>(value: T | null | undefined): value is T {
174
+ return value !== null && value !== undefined
175
+ }
176
+
177
+ function normalizeSchemaInput(base: SanitizeSchemaInput): SanitizeSchema {
178
+ return {
179
+ ...(isPresent(base.allowComments) && { allowComments: base.allowComments }),
180
+ ...(isPresent(base.allowDoctypes) && { allowDoctypes: base.allowDoctypes }),
181
+ ...(isPresent(base.ancestors) && { ancestors: base.ancestors }),
182
+ ...(isPresent(base.attributes) && { attributes: base.attributes }),
183
+ ...(isPresent(base.clobber) && { clobber: base.clobber }),
184
+ ...(isPresent(base.clobberPrefix) && { clobberPrefix: base.clobberPrefix }),
185
+ ...(isPresent(base.protocols) && { protocols: normalizeProtocols(base.protocols) }),
186
+ ...(isPresent(base.required) && { required: base.required }),
187
+ ...(isPresent(base.strip) && { strip: base.strip }),
188
+ ...(isPresent(base.tagNames) && { tagNames: base.tagNames }),
189
+ }
190
+ }
191
+
192
+ function normalizeProtocols(
193
+ protocols: Readonly<Record<string, readonly string[] | null | undefined>>,
194
+ ): Readonly<Record<string, readonly string[]>> {
195
+ const out = new Map<string, readonly string[]>()
196
+ for (const [key, values] of Object.entries(protocols)) {
197
+ if (isPresent(values)) out.set(key, values)
198
+ }
199
+ return Object.fromEntries(out)
200
+ }
201
+
202
+ // ── Attribute-definition algebra (mirrors upstream's lookup) ──────────────────────────────────
203
+
204
+ /** The property an entry governs — a bare string IS the name, a tuple carries it at index 0. */
205
+ function definitionName(definition: SanitizePropertyDefinition): string {
206
+ return typeof definition === 'string' ? definition : definition[0]
207
+ }
208
+
209
+ /** Upstream only treats an entry as an allow-list when it is a tuple with `length > 1`. */
210
+ function definitionAllowsAnyValue(definition: SanitizePropertyDefinition): boolean {
211
+ return typeof definition === 'string' || definition.length < 2
212
+ }
213
+
214
+ function definitionValues(
215
+ definition: SanitizePropertyDefinition,
216
+ ): readonly (boolean | number | string | RegExp | null | undefined)[] {
217
+ return typeof definition === 'string' ? [] : definition.slice(1)
218
+ }
219
+
220
+ /**
221
+ * De-duplication key for one allow-list value. Expressions are keyed off `String(expression)`
222
+ * (`/^hljs-/i` → `"/^hljs-/i"`), which encodes both source and flags and — unlike `instanceof` —
223
+ * survives a cross-realm `RegExp`, matching upstream's own duck-typed `'flags' in allowed` test.
224
+ */
225
+ function definitionValueKey(value: boolean | number | string | RegExp | null | undefined): string {
226
+ if (typeof value === 'object' && value !== null) return `expression${String(value)}`
227
+ return `${typeof value}${String(value)}`
228
+ }
229
+
230
+ function toDefinition(
231
+ name: string,
232
+ values: readonly (boolean | number | string | RegExp | null | undefined)[],
233
+ ): SanitizePropertyDefinition {
234
+ return values.length === 0 ? name : [name, ...values]
235
+ }
236
+
237
+ /**
238
+ * Collapses a list of entries to AT MOST ONE ENTRY PER PROPERTY NAME, unioning their allow-lists.
239
+ *
240
+ * This is the merge's core, and it exists because upstream's `findDefinition` returns the FIRST
241
+ * entry matching a property name — so a second entry for that same name is dead code, and merging
242
+ * by concatenation would make every consumer addition to an already-mentioned property a silent
243
+ * no-op. Union semantics, in the only direction an additions-only API may move:
244
+ *
245
+ * - allow-any (a bare string, or a one-element tuple) absorbs everything — union-ing it with a
246
+ * tuple yields allow-any, never the tuple's narrower list. An extension cannot NARROW.
247
+ * - two tuples yield one tuple whose values are the concatenation, de-duplicated by
248
+ * `definitionValueKey` (expressions structurally, primitives by value and type).
249
+ *
250
+ * Order is base-first, by first appearance of each property name, so output is stable.
251
+ */
252
+ function unionDefinitions(
253
+ definitions: readonly SanitizePropertyDefinition[],
254
+ ): readonly SanitizePropertyDefinition[] {
255
+ type Slot = {
256
+ allowAny: boolean
257
+ values: (boolean | number | string | RegExp | null | undefined)[]
258
+ keys: Set<string>
259
+ }
260
+ const order: string[] = []
261
+ const byName = new Map<string, Slot>()
262
+
263
+ for (const definition of definitions) {
264
+ const name = definitionName(definition)
265
+ let slot = byName.get(name)
266
+ if (!slot) {
267
+ slot = { allowAny: false, values: [], keys: new Set<string>() }
268
+ byName.set(name, slot)
269
+ order.push(name)
270
+ }
271
+ if (definitionAllowsAnyValue(definition)) {
272
+ slot.allowAny = true
273
+ continue
274
+ }
275
+ for (const value of definitionValues(definition)) {
276
+ const key = definitionValueKey(value)
277
+ if (slot.keys.has(key)) continue
278
+ slot.keys.add(key)
279
+ slot.values.push(value)
280
+ }
281
+ }
282
+
283
+ const out: SanitizePropertyDefinition[] = []
284
+ for (const name of order) {
285
+ const slot = byName.get(name)
286
+ if (!slot) continue
287
+ out.push(slot.allowAny ? name : toDefinition(name, slot.values))
288
+ }
289
+ return out
290
+ }
291
+
292
+ /**
293
+ * Upstream's `findDefinition`, mirrored: first exact name match wins, otherwise a `'data*'` entry
294
+ * answers for any `data`-prefixed key. Used to locate what the `'*'` fallback would have granted.
295
+ */
296
+ function findFallbackDefinition(
297
+ definitions: readonly SanitizePropertyDefinition[] | undefined,
298
+ key: string,
299
+ ): SanitizePropertyDefinition | undefined {
300
+ let dataDefault: SanitizePropertyDefinition | undefined
301
+ for (const definition of definitions ?? []) {
302
+ const name = definitionName(definition)
303
+ if (name === key) return definition
304
+ if (name === 'data*') dataDefault = definition
305
+ }
306
+ if (key.length > 4 && key.slice(0, 4).toLowerCase() === 'data') return dataDefault
307
+ return undefined
308
+ }
309
+
310
+ /** Re-points a `'*'` entry (possibly the `'data*'` catch-all) at a concrete property name. */
311
+ function renameDefinition(
312
+ definition: SanitizePropertyDefinition,
313
+ name: string,
314
+ ): SanitizePropertyDefinition {
315
+ if (definitionName(definition) === name) return definition
316
+ return definitionAllowsAnyValue(definition)
317
+ ? name
318
+ : toDefinition(name, definitionValues(definition))
319
+ }
320
+
321
+ /**
322
+ * Materializes the `'*'` allowances an addition would otherwise SHADOW OFF a tag.
323
+ *
324
+ * Upstream consults the `'*'` fallback only when the tag-specific lookup returns `null`/undefined —
325
+ * and for an array-valued property a matching tag-specific entry returns `[]`, which is neither.
326
+ * So the moment an extension names property P on tag T, T stops inheriting `'*'`'s P allowance for
327
+ * array values. Rescuing it here (so the union below carries it) is what keeps a widening from
328
+ * being a removal.
329
+ *
330
+ * Only names the extension INTRODUCES to T are rescued: a name T's own base list already carried
331
+ * was already shadowing `'*'` before the merge, so leaving it alone preserves base behaviour
332
+ * exactly rather than widening it.
333
+ */
334
+ function rescueWildcardDefinitions({
335
+ wildcard,
336
+ current,
337
+ additions,
338
+ }: {
339
+ readonly wildcard: readonly SanitizePropertyDefinition[] | undefined
340
+ readonly current: readonly SanitizePropertyDefinition[] | undefined
341
+ readonly additions: readonly SanitizePropertyDefinition[]
342
+ }): readonly SanitizePropertyDefinition[] {
343
+ if (!wildcard || wildcard.length === 0) return []
344
+
345
+ const shadowed = new Set((current ?? []).map(definitionName))
346
+ const rescued: SanitizePropertyDefinition[] = []
347
+ for (const definition of additions) {
348
+ const name = definitionName(definition)
349
+ if (shadowed.has(name)) continue
350
+ shadowed.add(name)
351
+ const fallback = findFallbackDefinition(wildcard, name)
352
+ if (fallback !== undefined) rescued.push(renameDefinition(fallback, name))
353
+ }
354
+ return rescued
355
+ }
356
+
357
+ // ── Additive merge ────────────────────────────────────────────────────────────────────────────
358
+
359
+ /** Base entries first in their original order, then each addition not already present. */
360
+ function concatUniqueStrings(
361
+ base: readonly string[] | undefined,
362
+ additions: readonly string[],
363
+ ): readonly string[] {
364
+ const out = base ? [...base] : []
365
+ const seen = new Set(out)
366
+ for (const value of additions) {
367
+ if (seen.has(value)) continue
368
+ seen.add(value)
369
+ out.push(value)
370
+ }
371
+ return out
372
+ }
373
+
374
+ /**
375
+ * Merges a record-of-lists PER KEY: a key in both ends up with the base list AND the addition's
376
+ * list concatenated, never the addition's replacing the base's. Built through a `Map` +
377
+ * `Object.fromEntries` so key order stays base-first and a `__proto__` key in an addition defines
378
+ * an own property rather than reaching the `Object.prototype` setter.
379
+ */
380
+ function mergeRecordOfLists<T>(
381
+ base: Readonly<Record<string, readonly T[]>> | undefined,
382
+ addition: Readonly<Record<string, readonly T[]>>,
383
+ concat: (base: readonly T[] | undefined, additions: readonly T[]) => readonly T[],
384
+ ): Readonly<Record<string, readonly T[]>> {
385
+ const merged = new Map<string, readonly T[]>(Object.entries(base ?? {}))
386
+ for (const [key, values] of Object.entries(addition)) {
387
+ merged.set(key, concat(merged.get(key), values))
388
+ }
389
+ return Object.fromEntries(merged)
390
+ }
391
+
392
+ /**
393
+ * Merges `attributes` per tag. Unlike the plain record-of-lists merge this has to model two
394
+ * upstream lookup rules at once — one entry per property name (`unionDefinitions`) and the `'*'`
395
+ * fallback that a tag-specific entry switches off (`rescueWildcardDefinitions`).
396
+ *
397
+ * `'*'` is processed FIRST so a wildcard widened by the same extension object is already visible to
398
+ * the per-tag rescue, making the result independent of the addition's key order.
399
+ */
400
+ function mergeAttributes(
401
+ base: Readonly<Record<string, readonly SanitizePropertyDefinition[]>> | undefined,
402
+ addition: Readonly<Record<string, readonly SanitizePropertyDefinition[]>>,
403
+ ): Readonly<Record<string, readonly SanitizePropertyDefinition[]>> {
404
+ const merged = new Map<string, readonly SanitizePropertyDefinition[]>(Object.entries(base ?? {}))
405
+ const tags = Object.keys(addition)
406
+ const ordered = tags.includes(WILDCARD)
407
+ ? [WILDCARD, ...tags.filter((tag) => tag !== WILDCARD)]
408
+ : tags
409
+
410
+ for (const tag of ordered) {
411
+ const additions = addition[tag] ?? []
412
+ const current = merged.get(tag)
413
+ if (tag === WILDCARD) {
414
+ merged.set(tag, unionDefinitions([...(current ?? []), ...additions]))
415
+ continue
416
+ }
417
+ const rescued = rescueWildcardDefinitions({
418
+ wildcard: merged.get(WILDCARD),
419
+ current,
420
+ additions,
421
+ })
422
+ merged.set(tag, unionDefinitions([...(current ?? []), ...rescued, ...additions]))
423
+ }
424
+ return Object.fromEntries(merged)
425
+ }
426
+
427
+ /**
428
+ * Applies an extension's `clobberPrefix`, IGNORING anything that is not a non-empty string.
429
+ *
430
+ * Upstream gates prefixing on `schema.clobber && schema.clobberPrefix && …` — a TRUTHINESS check,
431
+ * not `=== ''`. So `''` is not the only disarming value: `null`, `0`, `false`, and `NaN` all read as
432
+ * falsy too and switch clobber protection off entirely, letting `id`/`name` back out unprefixed.
433
+ * That is the DOM-clobbering vector (`<a id="body">`) the default prefix exists to close, and none
434
+ * of its spellings may be reachable through an additions-only API.
435
+ *
436
+ * `next` is typed `unknown` rather than trusting `SanitizeSchemaExtension`'s declared `string` —
437
+ * the type is a compile-time claim, and this is a runtime security boundary: a plain-JS caller (no
438
+ * type checker in the loop) or an `as` cast can hand this function `null`/`0`/`false`/`NaN`/an
439
+ * object despite the type, so the enforcement has to `typeof`-check rather than assume. Only a
440
+ * genuine non-empty string is adopted; everything else — including non-string truthy values, which
441
+ * upstream would coerce/misuse rather than treat as a prefix — is refused with the existing
442
+ * dev-warn and the previous prefix is kept.
443
+ */
444
+ function adoptClobberPrefix(current: string | undefined, next: unknown): string | undefined {
445
+ if (typeof next === 'string' && next !== '') return next
446
+ if (process.env['NODE_ENV'] !== 'production') {
447
+ console.warn(
448
+ '[basalt] mergeSanitizeSchema: ignoring an invalid `clobberPrefix`. hast-util-sanitize gates ' +
449
+ 'clobber prefixing on truthiness, so a falsy value (or a non-string) disables it entirely ' +
450
+ 'and emits id/name unprefixed — the DOM-clobbering hole "user-content-" exists to close. ' +
451
+ 'The previous prefix is kept.',
452
+ )
453
+ }
454
+ return current
455
+ }
456
+
457
+ /**
458
+ * Deep-merges additions into a base schema. Additive ONLY — and "additive" is measured by what the
459
+ * SANITIZER then allows, not by array lengths (see the module JSDoc for the three upstream lookup
460
+ * rules this has to model):
461
+ *
462
+ * - `tagNames` and `protocols` are concatenated per key, de-duplicated.
463
+ * - `attributes` are merged per tag into ONE entry per property name whose allow-list is the union
464
+ * of base and addition — and a tag gaining its first entry for a property keeps whatever `'*'`
465
+ * granted it, so a widening never shadows the wildcard off.
466
+ * - `clobberPrefix` is the single replace-valued field (a later extension wins; an omitted one
467
+ * leaves the previous value intact) — except the empty string, which is ignored.
468
+ *
469
+ * Never removes anything from `base`: keys no extension mentions (`strip`, `clobber`, `ancestors`,
470
+ * `required`, `allowComments`, `allowDoctypes`) pass through untouched.
471
+ *
472
+ * The `base` parameter takes the nullable `SanitizeSchemaInput` so `rehype-sanitize`'s own
473
+ * `defaultSchema` assigns with no cast; the RETURN type stays non-null.
474
+ *
475
+ * Pure and order-stable: no input is mutated, and the output ordering is a deterministic function
476
+ * of the inputs, so snapshots don't churn.
477
+ *
478
+ * @example
479
+ * mergeSanitizeSchema(defaultSchema, BASALT_SANITIZE_SCHEMA, { tagNames: ['hermes-badge'] })
480
+ */
481
+ export function mergeSanitizeSchema(
482
+ base: SanitizeSchemaInput,
483
+ ...extensions: readonly (SanitizeSchemaExtension | undefined)[]
484
+ ): SanitizeSchema {
485
+ const normalized = normalizeSchemaInput(base)
486
+ let tagNames = normalized.tagNames
487
+ let attributes = normalized.attributes
488
+ let protocols = normalized.protocols
489
+ let clobberPrefix = normalized.clobberPrefix
490
+
491
+ for (const extension of extensions) {
492
+ if (!extension) continue
493
+ if (extension.tagNames) tagNames = concatUniqueStrings(tagNames, extension.tagNames)
494
+ if (extension.attributes) attributes = mergeAttributes(attributes, extension.attributes)
495
+ if (extension.protocols) {
496
+ protocols = mergeRecordOfLists(protocols, extension.protocols, concatUniqueStrings)
497
+ }
498
+ if (extension.clobberPrefix !== undefined) {
499
+ clobberPrefix = adoptClobberPrefix(clobberPrefix, extension.clobberPrefix)
500
+ }
501
+ }
502
+
503
+ return {
504
+ ...normalized,
505
+ ...(tagNames !== undefined && { tagNames }),
506
+ ...(attributes !== undefined && { attributes }),
507
+ ...(protocols !== undefined && { protocols }),
508
+ ...(clobberPrefix !== undefined && { clobberPrefix }),
509
+ }
510
+ }
@@ -28,7 +28,12 @@ import type { StatCardTone } from './stat-card'
28
28
  function render(tone?: StatCardTone) {
29
29
  return renderToStaticMarkup(
30
30
  <MantineProvider>
31
- <StatCard label="Downtime · last 24h" value="0 min" tone={tone} />
31
+ {/* exactOptionalPropertyTypes forbids an explicit tone={undefined} — spread only when set */}
32
+ <StatCard
33
+ label="Downtime · last 24h"
34
+ value="0 min"
35
+ {...(tone !== undefined ? { tone } : {})}
36
+ />
32
37
  </MantineProvider>,
33
38
  )
34
39
  }
@@ -209,11 +209,15 @@ describe('mantine-shade-index', () => {
209
209
  })
210
210
 
211
211
  // The grace-minor doctrine (package CLAUDE.md): a kind that rejects previously-passing code
212
- // lands as `warn` for one minor. Deleting its GRACE_PERIOD_KINDS entry flips this — scheduled
213
- // for 1.9.0, deferred one minor because 1.8.0 shipped the same day as 1.7.0 (see the entry).
214
- it('lands as a warning, not an error, for its grace minor', () => {
212
+ // lands as `warn` for one minor, then its GRACE_PERIOD_KINDS entry is deleted and it becomes an
213
+ // error. This one ran its grace across FOUR minors — introduced 1.7.0, then deferred by 1.8.0
214
+ // (shipped the same day as 1.7.0), 1.9.0 (carried the chart-layer batch the same consumer was
215
+ // waiting on) and 1.10.0 (shipped without the promotion at all). Promoted in 1.11.0, verified
216
+ // against the only consumer first: argo's `check-theme` reports zero violations of any kind, so
217
+ // the promotion breaks nothing that was passing.
218
+ it('is an error now that its grace period has ended', () => {
215
219
  const f = find(`<Text c="yellow.7" />`)
216
- expect(f.find((x) => x.kind === 'mantine-shade-index')?.severity).toBe('warn')
220
+ expect(f.find((x) => x.kind === 'mantine-shade-index')?.severity).toBe('error')
217
221
  })
218
222
  })
219
223
 
@@ -296,9 +296,7 @@ export const DEFAULT_GUARD_CONFIG: GuardConfig = {
296
296
  * 'raw-font-family': 'introduced 1.4.0 — promote to error in 1.5.0',
297
297
  * }
298
298
  */
299
- const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, string>> = {
300
- 'mantine-shade-index': 'introduced 1.7.0 — promote to error in 1.10.0',
301
- }
299
+ const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, string>> = {}
302
300
 
303
301
  /** A kind's effective severity: consumer override first, then the grace table, then `error`. */
304
302
  function severityOf(kind: GuardKind, cfg: GuardConfig): GuardSeverity {
package/src/index.ts CHANGED
@@ -118,6 +118,7 @@ export {
118
118
  Composer,
119
119
  threadPartRenderers,
120
120
  ThreadTranscript,
121
+ ToolChip,
121
122
  } from './agent-chat'
122
123
  export type {
123
124
  ThreadWorkspaceProps,
@@ -125,14 +126,27 @@ export type {
125
126
  ThreadOutcomeCardProps,
126
127
  ThreadDetailPanelProps,
127
128
  ComposerProps,
129
+ ComposerSubmit,
130
+ ComposerAttachment,
131
+ ComposerHandle,
128
132
  ThreadTranscriptProps,
133
+ ToolChipProps,
129
134
  } from './agent-chat'
130
- export { createThreadsStore, heuristicOutcome, useAgentThreadRuns } from './agent'
135
+ export {
136
+ createAdapterThreadsStore,
137
+ createThreadsStore,
138
+ heuristicOutcome,
139
+ threadsStoreAdapterContract,
140
+ useAgentThreadRuns,
141
+ } from './agent'
131
142
  export type {
143
+ AdapterThreadsStoreOptions,
132
144
  AgentThread,
133
145
  AgentOutcome,
134
146
  ThreadStatus,
135
147
  ThreadsStore,
148
+ ThreadsStoreAdapter,
149
+ ThreadsStoreAdapterContractCase,
136
150
  ThreadsStoreOptions,
137
151
  OutcomeResolver,
138
152
  ChatMessage,
package/src/surfaces.ts CHANGED
@@ -195,7 +195,14 @@ export const SURFACES = {
195
195
  ],
196
196
  description:
197
197
  'BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, SettingsSection/SettingsRow/DangerZone)',
198
- optionalPeers: ['react-markdown', 'remark-gfm', 'shiki', 'beautiful-mermaid'],
198
+ optionalPeers: [
199
+ 'react-markdown',
200
+ 'remark-gfm',
201
+ 'rehype-sanitize',
202
+ 'remend',
203
+ 'shiki',
204
+ 'beautiful-mermaid',
205
+ ],
199
206
  forbiddenImports: [], // the no-charts/tokens-reexport invariant is comment-only today; Phase-4 plugin
200
207
  },
201
208
  './charts': {
@@ -362,7 +369,7 @@ export const SURFACES = {
362
369
  skill: ['basalt-app'],
363
370
  guardKinds: [],
364
371
  description:
365
- 'Headless streaming-chat layer: useAgentStream, aiSdkTransport (recommended default) + edenTransport, PartList, plus the multi-thread createThreadsStore + useAgentThreadRuns + outcome-resolver seam (Mantine-free)',
372
+ 'Headless streaming-chat layer: useAgentStream, aiSdkTransport (recommended default) + edenTransport, isResumable/ResumableAgentTransport (stream-resumption seam), PartList, coalesceParts, the ForeignPart/definePartRenderers/narrowAgentPart open part-registry seam, plus the multi-thread createThreadsStore + useAgentThreadRuns + outcome-resolver seam (Mantine-free)',
366
373
  optionalPeers: ['ai', 'use-stick-to-bottom'],
367
374
  globs: {
368
375
  shipped: [],
@@ -378,7 +385,7 @@ export const SURFACES = {
378
385
  skill: ['basalt-app'],
379
386
  guardKinds: [],
380
387
  description:
381
- 'Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed, ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript, threadPartRenderers (Mantine-coupled). remend and motion are required, not optional — ThreadTranscript imports content/markdown.tsx (remend eagerly), and ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without both installed even though peerDependenciesMeta marks them optional (npm has no per-subpath optionality).',
388
+ 'Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed, ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript (open part-renderer registry via its renderers/fallbackRenderer props), threadPartRenderers, ToolChip (Mantine-coupled). motion is required, not optional — ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without it installed even though peerDependenciesMeta marks it optional (npm has no per-subpath optionality). remend is genuinely optional here — ThreadTranscript reaches it only through the lazy dynamic import() inside content/markdown.tsx.',
382
389
  optionalPeers: [
383
390
  'ai',
384
391
  'motion',
@@ -400,7 +407,7 @@ export const SURFACES = {
400
407
  skill: ['basalt-design'],
401
408
  guardKinds: [],
402
409
  description:
403
- 'Prose (article/chat typography), CodeBlock (shiki, optional peer), Callout, TableOfContents, ReadingProgress, Markdown (react-markdown + remark-gfm, optional peers, streaming-aware), MermaidDiagram (beautiful-mermaid, optional peer), mdxComponents/createMdxComponents, ArticleLayout (docs-page frame), ArticleCard/ArticleGrid (overview cards), Article model (sortArticles/filterArticles/formatArticleDate), ArticleFilterBar (category/tags filter UI), toArticleActions (Spotlight projector, @mantine/spotlight type-only), GuideLink/GuideDrawer (contextual-help drawer) — the content/prose surface',
410
+ 'Prose (article/chat typography), CodeBlock (shiki, optional peer), Callout, TableOfContents, ReadingProgress, Markdown (react-markdown + remark-gfm, optional peers; `streaming` is a rendering mode ONLY — `contentTrust` is the independent security input, and any surface rendering agent/model output must pin `contentTrust="untrusted"`, the sole input to the image-origin allowlist; a `fenceRenderers` registry — settledOnly/FenceRenderer/FenceRenderers/FenceRenderContext; `sanitizeSchema`, an additions-only SanitizeSchemaExtension merged over BASALT_SANITIZE_SCHEMA via mergeSanitizeSchema; the remend streaming-repair pass is now a lazy optional peer), MermaidDiagram (beautiful-mermaid, optional peer), mdxComponents/createMdxComponents, ArticleLayout (docs-page frame), ArticleCard/ArticleGrid (overview cards), Article model (sortArticles/filterArticles/formatArticleDate), ArticleFilterBar (category/tags filter UI), toArticleActions (Spotlight projector, @mantine/spotlight type-only), GuideLink/GuideDrawer (contextual-help drawer) — the content/prose surface',
404
411
  optionalPeers: [
405
412
  'shiki',
406
413
  '@shikijs/langs',
@@ -408,6 +415,8 @@ export const SURFACES = {
408
415
  'beautiful-mermaid',
409
416
  'react-markdown',
410
417
  'remark-gfm',
418
+ 'rehype-sanitize',
419
+ 'remend',
411
420
  '@mantine/spotlight',
412
421
  ],
413
422
  forbiddenImports: [],