@oxy.so/bloom 3.0.0 → 3.1.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 (271) hide show
  1. package/docs/adoption-matrix.mdx +266 -0
  2. package/docs/checkbox.mdx +16 -0
  3. package/docs/composition.mdx +88 -16
  4. package/docs/date-picker.mdx +11 -0
  5. package/docs/field.mdx +35 -4
  6. package/docs/file-upload.mdx +13 -0
  7. package/docs/glass.mdx +15 -0
  8. package/docs/input-group.mdx +22 -0
  9. package/docs/input-otp.mdx +14 -0
  10. package/docs/layout.mdx +34 -0
  11. package/docs/migrating-to-3.0.mdx +43 -4
  12. package/docs/page-header.mdx +46 -1
  13. package/docs/phone-input.mdx +17 -0
  14. package/docs/radio.mdx +16 -0
  15. package/docs/rating.mdx +14 -0
  16. package/docs/segmented-control.mdx +16 -4
  17. package/docs/select.mdx +22 -0
  18. package/docs/slider.mdx +14 -0
  19. package/docs/stepper.mdx +3 -1
  20. package/docs/switch.mdx +14 -0
  21. package/docs/text-field.mdx +14 -0
  22. package/docs/textarea.mdx +20 -0
  23. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +11 -4
  24. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  25. package/lib/commonjs/checkbox/Checkbox.js +24 -8
  26. package/lib/commonjs/checkbox/Checkbox.js.map +1 -1
  27. package/lib/commonjs/date-picker/TimeField.js +18 -6
  28. package/lib/commonjs/date-picker/TimeField.js.map +1 -1
  29. package/lib/commonjs/dialog/DialogContent.js +36 -29
  30. package/lib/commonjs/dialog/DialogContent.js.map +1 -1
  31. package/lib/commonjs/field/index.js +7 -0
  32. package/lib/commonjs/field/index.js.map +1 -1
  33. package/lib/commonjs/field/membership.js +105 -0
  34. package/lib/commonjs/field/membership.js.map +1 -0
  35. package/lib/commonjs/file-upload/FileUpload.js +17 -3
  36. package/lib/commonjs/file-upload/FileUpload.js.map +1 -1
  37. package/lib/commonjs/index.js +21 -0
  38. package/lib/commonjs/index.js.map +1 -1
  39. package/lib/commonjs/index.web.js +21 -0
  40. package/lib/commonjs/index.web.js.map +1 -1
  41. package/lib/commonjs/input-group/InputGroup.js +18 -5
  42. package/lib/commonjs/input-group/InputGroup.js.map +1 -1
  43. package/lib/commonjs/input-otp/InputOtp.js +20 -8
  44. package/lib/commonjs/input-otp/InputOtp.js.map +1 -1
  45. package/lib/commonjs/layout/index.js +7 -0
  46. package/lib/commonjs/layout/index.js.map +1 -1
  47. package/lib/commonjs/layout/screen-scope.js +61 -0
  48. package/lib/commonjs/layout/screen-scope.js.map +1 -0
  49. package/lib/commonjs/phone-input/PhoneInput.js +20 -8
  50. package/lib/commonjs/phone-input/PhoneInput.js.map +1 -1
  51. package/lib/commonjs/radio/Radio.js +35 -9
  52. package/lib/commonjs/radio/Radio.js.map +1 -1
  53. package/lib/commonjs/rating/RatingInput.js +14 -6
  54. package/lib/commonjs/rating/RatingInput.js.map +1 -1
  55. package/lib/commonjs/segmented-control/SegmentedControl.js +38 -8
  56. package/lib/commonjs/segmented-control/SegmentedControl.js.map +1 -1
  57. package/lib/commonjs/select/Select.js +15 -3
  58. package/lib/commonjs/select/Select.js.map +1 -1
  59. package/lib/commonjs/select/Select.web.js +13 -5
  60. package/lib/commonjs/select/Select.web.js.map +1 -1
  61. package/lib/commonjs/slider/Slider.js +17 -3
  62. package/lib/commonjs/slider/Slider.js.map +1 -1
  63. package/lib/commonjs/stepper/Stepper.js +16 -7
  64. package/lib/commonjs/stepper/Stepper.js.map +1 -1
  65. package/lib/commonjs/styles/styled-primitives.js.map +1 -1
  66. package/lib/commonjs/switch/Switch.js +23 -8
  67. package/lib/commonjs/switch/Switch.js.map +1 -1
  68. package/lib/commonjs/text-field/TextField.js +19 -4
  69. package/lib/commonjs/text-field/TextField.js.map +1 -1
  70. package/lib/commonjs/textarea/Textarea.js +39 -15
  71. package/lib/commonjs/textarea/Textarea.js.map +1 -1
  72. package/lib/module/bottom-sheet/BottomSheetBase.js +8 -1
  73. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  74. package/lib/module/checkbox/Checkbox.js +24 -8
  75. package/lib/module/checkbox/Checkbox.js.map +1 -1
  76. package/lib/module/date-picker/TimeField.js +19 -5
  77. package/lib/module/date-picker/TimeField.js.map +1 -1
  78. package/lib/module/dialog/DialogContent.js +37 -30
  79. package/lib/module/dialog/DialogContent.js.map +1 -1
  80. package/lib/module/field/index.js +1 -0
  81. package/lib/module/field/index.js.map +1 -1
  82. package/lib/module/field/membership.js +101 -0
  83. package/lib/module/field/membership.js.map +1 -0
  84. package/lib/module/file-upload/FileUpload.js +17 -3
  85. package/lib/module/file-upload/FileUpload.js.map +1 -1
  86. package/lib/module/index.js +2 -2
  87. package/lib/module/index.js.map +1 -1
  88. package/lib/module/index.web.js +2 -2
  89. package/lib/module/index.web.js.map +1 -1
  90. package/lib/module/input-group/InputGroup.js +19 -4
  91. package/lib/module/input-group/InputGroup.js.map +1 -1
  92. package/lib/module/input-otp/InputOtp.js +21 -7
  93. package/lib/module/input-otp/InputOtp.js.map +1 -1
  94. package/lib/module/layout/index.js +4 -1
  95. package/lib/module/layout/index.js.map +1 -1
  96. package/lib/module/layout/screen-scope.js +57 -0
  97. package/lib/module/layout/screen-scope.js.map +1 -0
  98. package/lib/module/phone-input/PhoneInput.js +22 -9
  99. package/lib/module/phone-input/PhoneInput.js.map +1 -1
  100. package/lib/module/radio/Radio.js +35 -9
  101. package/lib/module/radio/Radio.js.map +1 -1
  102. package/lib/module/rating/RatingInput.js +15 -5
  103. package/lib/module/rating/RatingInput.js.map +1 -1
  104. package/lib/module/segmented-control/SegmentedControl.js +38 -8
  105. package/lib/module/segmented-control/SegmentedControl.js.map +1 -1
  106. package/lib/module/select/Select.js +15 -3
  107. package/lib/module/select/Select.js.map +1 -1
  108. package/lib/module/select/Select.web.js +14 -4
  109. package/lib/module/select/Select.web.js.map +1 -1
  110. package/lib/module/slider/Slider.js +17 -3
  111. package/lib/module/slider/Slider.js.map +1 -1
  112. package/lib/module/stepper/Stepper.js +17 -6
  113. package/lib/module/stepper/Stepper.js.map +1 -1
  114. package/lib/module/styles/styled-primitives.js.map +1 -1
  115. package/lib/module/switch/Switch.js +23 -8
  116. package/lib/module/switch/Switch.js.map +1 -1
  117. package/lib/module/text-field/TextField.js +19 -4
  118. package/lib/module/text-field/TextField.js.map +1 -1
  119. package/lib/module/textarea/Textarea.js +39 -15
  120. package/lib/module/textarea/Textarea.js.map +1 -1
  121. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/checkbox/Checkbox.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/checkbox/types.d.ts +5 -0
  124. package/lib/typescript/commonjs/checkbox/types.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/date-picker/TimeField.d.ts +1 -1
  126. package/lib/typescript/commonjs/date-picker/TimeField.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/date-picker/types.d.ts +6 -3
  128. package/lib/typescript/commonjs/date-picker/types.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/dialog/DialogContent.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/field/index.d.ts +2 -0
  131. package/lib/typescript/commonjs/field/index.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/field/membership.d.ts +60 -0
  133. package/lib/typescript/commonjs/field/membership.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/file-upload/FileUpload.d.ts +1 -1
  135. package/lib/typescript/commonjs/file-upload/FileUpload.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/floating/types.d.ts +13 -0
  137. package/lib/typescript/commonjs/floating/types.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/index.d.ts +3 -3
  139. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/index.web.d.ts +3 -3
  141. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/input-group/InputGroup.d.ts +1 -1
  143. package/lib/typescript/commonjs/input-group/InputGroup.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/input-otp/InputOtp.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/input-otp/types.d.ts +7 -1
  146. package/lib/typescript/commonjs/input-otp/types.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/layout/index.d.ts +4 -1
  148. package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/layout/screen-scope.d.ts +40 -0
  150. package/lib/typescript/commonjs/layout/screen-scope.d.ts.map +1 -0
  151. package/lib/typescript/commonjs/phone-input/PhoneInput.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/radio/Radio.d.ts +1 -1
  153. package/lib/typescript/commonjs/radio/Radio.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/radio/types.d.ts +11 -3
  155. package/lib/typescript/commonjs/radio/types.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/rating/RatingInput.d.ts +1 -1
  157. package/lib/typescript/commonjs/rating/RatingInput.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/rating/types.d.ts +6 -3
  159. package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
  160. package/lib/typescript/commonjs/segmented-control/SegmentedControl.d.ts +18 -2
  161. package/lib/typescript/commonjs/segmented-control/SegmentedControl.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/select/Select.d.ts.map +1 -1
  163. package/lib/typescript/commonjs/select/Select.web.d.ts.map +1 -1
  164. package/lib/typescript/commonjs/select/types.d.ts +8 -1
  165. package/lib/typescript/commonjs/select/types.d.ts.map +1 -1
  166. package/lib/typescript/commonjs/slider/Slider.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/stepper/Stepper.d.ts +1 -1
  168. package/lib/typescript/commonjs/stepper/Stepper.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/stepper/types.d.ts +8 -1
  170. package/lib/typescript/commonjs/stepper/types.d.ts.map +1 -1
  171. package/lib/typescript/commonjs/styles/styled-primitives.d.ts +16 -0
  172. package/lib/typescript/commonjs/styles/styled-primitives.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/switch/Switch.d.ts.map +1 -1
  174. package/lib/typescript/commonjs/switch/types.d.ts +7 -0
  175. package/lib/typescript/commonjs/switch/types.d.ts.map +1 -1
  176. package/lib/typescript/commonjs/text-field/TextField.d.ts.map +1 -1
  177. package/lib/typescript/commonjs/textarea/Textarea.d.ts +1 -1
  178. package/lib/typescript/commonjs/textarea/Textarea.d.ts.map +1 -1
  179. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  180. package/lib/typescript/module/checkbox/Checkbox.d.ts.map +1 -1
  181. package/lib/typescript/module/checkbox/types.d.ts +5 -0
  182. package/lib/typescript/module/checkbox/types.d.ts.map +1 -1
  183. package/lib/typescript/module/date-picker/TimeField.d.ts +1 -1
  184. package/lib/typescript/module/date-picker/TimeField.d.ts.map +1 -1
  185. package/lib/typescript/module/date-picker/types.d.ts +6 -3
  186. package/lib/typescript/module/date-picker/types.d.ts.map +1 -1
  187. package/lib/typescript/module/dialog/DialogContent.d.ts.map +1 -1
  188. package/lib/typescript/module/field/index.d.ts +2 -0
  189. package/lib/typescript/module/field/index.d.ts.map +1 -1
  190. package/lib/typescript/module/field/membership.d.ts +60 -0
  191. package/lib/typescript/module/field/membership.d.ts.map +1 -0
  192. package/lib/typescript/module/file-upload/FileUpload.d.ts +1 -1
  193. package/lib/typescript/module/file-upload/FileUpload.d.ts.map +1 -1
  194. package/lib/typescript/module/floating/types.d.ts +13 -0
  195. package/lib/typescript/module/floating/types.d.ts.map +1 -1
  196. package/lib/typescript/module/index.d.ts +3 -3
  197. package/lib/typescript/module/index.d.ts.map +1 -1
  198. package/lib/typescript/module/index.web.d.ts +3 -3
  199. package/lib/typescript/module/index.web.d.ts.map +1 -1
  200. package/lib/typescript/module/input-group/InputGroup.d.ts +1 -1
  201. package/lib/typescript/module/input-group/InputGroup.d.ts.map +1 -1
  202. package/lib/typescript/module/input-otp/InputOtp.d.ts.map +1 -1
  203. package/lib/typescript/module/input-otp/types.d.ts +7 -1
  204. package/lib/typescript/module/input-otp/types.d.ts.map +1 -1
  205. package/lib/typescript/module/layout/index.d.ts +4 -1
  206. package/lib/typescript/module/layout/index.d.ts.map +1 -1
  207. package/lib/typescript/module/layout/screen-scope.d.ts +40 -0
  208. package/lib/typescript/module/layout/screen-scope.d.ts.map +1 -0
  209. package/lib/typescript/module/phone-input/PhoneInput.d.ts.map +1 -1
  210. package/lib/typescript/module/radio/Radio.d.ts +1 -1
  211. package/lib/typescript/module/radio/Radio.d.ts.map +1 -1
  212. package/lib/typescript/module/radio/types.d.ts +11 -3
  213. package/lib/typescript/module/radio/types.d.ts.map +1 -1
  214. package/lib/typescript/module/rating/RatingInput.d.ts +1 -1
  215. package/lib/typescript/module/rating/RatingInput.d.ts.map +1 -1
  216. package/lib/typescript/module/rating/types.d.ts +6 -3
  217. package/lib/typescript/module/rating/types.d.ts.map +1 -1
  218. package/lib/typescript/module/segmented-control/SegmentedControl.d.ts +18 -2
  219. package/lib/typescript/module/segmented-control/SegmentedControl.d.ts.map +1 -1
  220. package/lib/typescript/module/select/Select.d.ts.map +1 -1
  221. package/lib/typescript/module/select/Select.web.d.ts.map +1 -1
  222. package/lib/typescript/module/select/types.d.ts +8 -1
  223. package/lib/typescript/module/select/types.d.ts.map +1 -1
  224. package/lib/typescript/module/slider/Slider.d.ts.map +1 -1
  225. package/lib/typescript/module/stepper/Stepper.d.ts +1 -1
  226. package/lib/typescript/module/stepper/Stepper.d.ts.map +1 -1
  227. package/lib/typescript/module/stepper/types.d.ts +8 -1
  228. package/lib/typescript/module/stepper/types.d.ts.map +1 -1
  229. package/lib/typescript/module/styles/styled-primitives.d.ts +16 -0
  230. package/lib/typescript/module/styles/styled-primitives.d.ts.map +1 -1
  231. package/lib/typescript/module/switch/Switch.d.ts.map +1 -1
  232. package/lib/typescript/module/switch/types.d.ts +7 -0
  233. package/lib/typescript/module/switch/types.d.ts.map +1 -1
  234. package/lib/typescript/module/text-field/TextField.d.ts.map +1 -1
  235. package/lib/typescript/module/textarea/Textarea.d.ts +1 -1
  236. package/lib/typescript/module/textarea/Textarea.d.ts.map +1 -1
  237. package/package.json +2 -1
  238. package/src/bottom-sheet/BottomSheetBase.tsx +6 -1
  239. package/src/checkbox/Checkbox.tsx +24 -8
  240. package/src/checkbox/types.ts +5 -0
  241. package/src/date-picker/TimeField.tsx +14 -3
  242. package/src/date-picker/types.ts +6 -3
  243. package/src/dialog/DialogContent.tsx +6 -2
  244. package/src/field/index.ts +2 -0
  245. package/src/field/membership.ts +164 -0
  246. package/src/file-upload/FileUpload.tsx +14 -3
  247. package/src/floating/types.ts +13 -0
  248. package/src/index.ts +9 -2
  249. package/src/index.web.ts +9 -2
  250. package/src/input-group/InputGroup.tsx +13 -2
  251. package/src/input-otp/InputOtp.tsx +19 -5
  252. package/src/input-otp/types.ts +7 -1
  253. package/src/layout/index.ts +4 -1
  254. package/src/layout/screen-scope.tsx +54 -0
  255. package/src/phone-input/PhoneInput.tsx +20 -8
  256. package/src/radio/Radio.tsx +32 -9
  257. package/src/radio/types.ts +11 -3
  258. package/src/rating/RatingInput.tsx +10 -3
  259. package/src/rating/types.ts +6 -3
  260. package/src/segmented-control/SegmentedControl.tsx +51 -8
  261. package/src/select/Select.tsx +14 -2
  262. package/src/select/Select.web.tsx +11 -2
  263. package/src/select/types.ts +8 -1
  264. package/src/slider/Slider.tsx +13 -3
  265. package/src/stepper/Stepper.tsx +12 -4
  266. package/src/stepper/types.ts +8 -1
  267. package/src/styles/styled-primitives.ts +16 -0
  268. package/src/switch/Switch.tsx +22 -9
  269. package/src/switch/types.ts +7 -0
  270. package/src/text-field/TextField.tsx +16 -4
  271. package/src/textarea/Textarea.tsx +38 -15
@@ -0,0 +1,266 @@
1
+ ---
2
+ title: Adoption matrix
3
+ description: Every family in Bloom against every composition contract that applies to it — derived from the source, with each shortfall named and justified.
4
+ order: 3
5
+ ---
6
+
7
+ {/* AUTO-GENERATED by scripts/generate-adoption-matrix.ts — do not edit. */}
8
+ {/* `bun run generate:adoption-matrix`; asserted byte-identical by src/__tests__/adoption-matrix.test.ts. */}
9
+
10
+ # Adoption matrix
11
+
12
+ The contracts in [Composition](/docs/composition) are worth something only where they are read. This page is that measurement, and it is derived from the source on every change rather than written down once — a matrix maintained by hand is wrong the first time a family changes without its row being updated, and a stale row reads exactly like a fresh one.
13
+
14
+ ## What is measured
15
+
16
+ - **Field membership** — `field/context.ts, field/membership.ts`. The id, accessible name, description, error, invalid, required and disabled state of an enclosing `Field`, applied to the control. Applies to a family that renders a raw `TextInput` or writes a form role of its own.
17
+ - **Control presentation** — `control-surface/context.ts`. The material and density a container asks the controls inside it for. Applies to a family that PAINTS a container material, or whose size prop is the density pair `medium | small`.
18
+ - **Trigger composition** — `floating/TriggerSlot.tsx`. `asChild`, the composed press handler, the cancellable open, the refused fragment and the two-sided disabled guard. Applies to a family that declares an `asChild` prop of its own.
19
+ - **Controlled / uncontrolled state** — `hooks/use-controllable-state.ts`. One reconciliation of "controlled the moment the prop is passed", with the callback firing either way. Applies to a family that declares both halves of a pair (`value` and `defaultValue`, `open` and `defaultOpen`, …).
20
+
21
+ A contract that a family does not read is not automatically a failure, and this is where a matrix usually starts lying in one direction or the other. `Search` renders no control of its own — it composes `TextFieldInput` — so the field contract is not its to read. A menu row carrying `role="checkbox"` is a menu item, not a form field. Every family a contract applies to and that does not read it is therefore classified, with a reason, in `src/__tests__/support/adoption-matrix.ts`:
22
+
23
+ | verdict | means |
24
+ | --- | --- |
25
+ | `reads` | the family imports the contract and applies it itself |
26
+ | `delegated` | it hands the decision to a child that reads it |
27
+ | `n/a¹` | the derivation matched something that is not the contract’s subject — the reason is below |
28
+ | `**adaptation**` | a real remaining gap, named rather than hidden |
29
+ | blank | the contract does not apply: the family renders nothing it governs |
30
+
31
+ The classification is an EQUALITY in `src/__tests__/adoption-matrix.test.ts`, not a floor: a family that starts matching a contract and is not classified renders as `**UNCLASSIFIED**` and fails the gate, and a classification whose family no longer matches fails it too.
32
+
33
+ ## Totals
34
+
35
+ - **161 families** in `src/`, **159** published (a subpath, the root barrel, or both). **33** carry a web fork.
36
+ - Docs, stories and suites are gated as a standard by `src/__tests__/family-coverage.test.ts`; the per-family counts are in the table below.
37
+
38
+ | contract | applies to | reads | delegated | n/a¹ | adaptation |
39
+ | --- | --- | --- | --- | --- | --- |
40
+ | Field membership | 34 | 16 | 0 | 15 | 3 |
41
+ | Control presentation | 14 | 7 | 1 | 6 | 0 |
42
+ | Trigger composition | 10 | 8 | 0 | 2 | 0 |
43
+ | Controlled / uncontrolled state | 38 | 30 | 0 | 1 | 7 |
44
+
45
+ ## What remains — the named adaptations
46
+
47
+ ### Field membership
48
+
49
+ - `listing-actions` — the mortgage calculator’s term picker is a hand-written `radiogroup`; it sits beside `TextFieldInput`s that DO read the contract, so one form has two association models.
50
+ - `listing-editor` — the property-type, offering and address-precision pickers are hand-written `radiogroup`s over cards, for the same reason and with the same cost as `questionnaire`.
51
+ - `questionnaire` — a FORM whose multi-select answers are hand-written `role="checkbox"` rows instead of `Checkbox`, so a `Field` can neither name nor disable them. Not closed here: the rows carry their own card chrome and selection model, so it is a redesign of the answer control rather than an import.
52
+
53
+ ### Controlled / uncontrolled state
54
+
55
+ - `chart-cards` — a card’s range selector reconciles `range`/`defaultRange` by hand, in three cards, each with its own copy.
56
+ - `chat-list` — the folder tabs reconcile `value`/`defaultValue` with their own `useState` seeded from the default, so a controlled caller’s later change to `value` is honoured but the callback and the internal state can disagree.
57
+ - `creator-studio` — the streams chart’s metric picker keeps its own state seeded from `defaultMetric` instead of the shared hook.
58
+ - `input-otp` — reconciles `value`/`defaultValue` by hand (`const controlled = value !== undefined`) because it also CLEANS and pads the string to `length`, which the shared hook does not do.
59
+ - `questionnaire` — the step and the answers are each reconciled by hand, which is two copies of the rule in one family.
60
+ - `queue-panel` — the panel’s tab keeps its own state seeded from `defaultTab` instead of the shared hook.
61
+ - `track-list` — the selection set keeps its own state seeded from `defaultSelectedIds` instead of the shared hook, which matters more here because the value is an ARRAY and identity decides whether a caller’s update lands.
62
+
63
+ ## ¹ Why a match is not a subject
64
+
65
+ ### Field membership
66
+
67
+ - `agent-chat` (does-not-apply) — renders a bare `TextInput` deliberately — a composer or a search box that draws its own chrome and is not a form field a `Field` wraps. Wiring it to `TextFieldInput` would give it the field shell it exists not to draw.
68
+ - `chat-composer` (does-not-apply) — renders a bare `TextInput` deliberately — a composer or a search box that draws its own chrome and is not a form field a `Field` wraps. Wiring it to `TextFieldInput` would give it the field shell it exists not to draw.
69
+ - `chat-list` (does-not-apply) — renders a bare `TextInput` deliberately — a composer or a search box that draws its own chrome and is not a form field a `Field` wraps. Wiring it to `TextFieldInput` would give it the field shell it exists not to draw.
70
+ - `chat-people` (does-not-apply) — writes a selection role on a ROW or a chip rather than on a form control — a list the user picks from, inside its own container, never inside a `Field`.
71
+ - `command` (does-not-apply) — renders a bare `TextInput` deliberately — a composer or a search box that draws its own chrome and is not a form field a `Field` wraps. Wiring it to `TextFieldInput` would give it the field shell it exists not to draw.
72
+ - `composer-panel` (does-not-apply) — its pills and effort slider are a toolbar of a composer, named by the group they sit in; the panel is never a field.
73
+ - `floating` (does-not-apply) — a MENU row. `checkbox`/`radio` inside a menu is the menu-item semantic, and the menu owns its own name, state and keyboard model (`floating/menu-rows.tsx`).
74
+ - `home-search` (does-not-apply) — renders a bare `TextInput` deliberately — a composer or a search box that draws its own chrome and is not a form field a `Field` wraps. Wiring it to `TextFieldInput` would give it the field shell it exists not to draw.
75
+ - `map-marker` (does-not-apply) — writes a selection role on a ROW or a chip rather than on a form control — a list the user picks from, inside its own container, never inside a `Field`.
76
+ - `media-controls` (does-not-apply) — a transport SCRUBBER (`adjustable`): it is named by the track it plays and lives in a player, not in a field.
77
+ - `message-media` (does-not-apply) — a voice message’s scrubber — same as `media-controls`.
78
+ - `music-library` (does-not-apply) — renders a bare `TextInput` deliberately — a composer or a search box that draws its own chrome and is not a form field a `Field` wraps. Wiring it to `TextFieldInput` would give it the field shell it exists not to draw.
79
+ - `sidebar` (does-not-apply) — renders a bare `TextInput` deliberately — a composer or a search box that draws its own chrome and is not a form field a `Field` wraps. Wiring it to `TextFieldInput` would give it the field shell it exists not to draw.
80
+ - `stay-filters` (does-not-apply) — writes a selection role on a ROW or a chip rather than on a form control — a list the user picks from, inside its own container, never inside a `Field`.
81
+ - `theme-toggle` (does-not-apply) — the switch row draws its own label ("Dark mode") as both visible text and accessible name, inside the toggle’s own menu.
82
+
83
+ ### Control presentation
84
+
85
+ - `chat-composer` (does-not-apply) — the `small | medium` prop sizes a composer PART (an attachment row), not a control a container would set the density of.
86
+ - `chat-people` (does-not-apply) — sizes an avatar row of its own, not a control: there is nothing for a container’s density to mean here, and honouring it would resize a presentation block.
87
+ - `chat-screen` (does-not-apply) — sizes the chat HEADER, which is chrome around the screen rather than a control a container sets the density of.
88
+ - `creator-studio` (does-not-apply) — sizes a studio stat block — a presentation part, whose two sizes happen to be spelled with the same two words as the density pair.
89
+ - `media-header` (does-not-apply) — sizes the media header’s meta row, which is layout of a header rather than a control inside a container.
90
+ - `phone-input` (delegated) — passes `size` straight through to `TextField`, which inherits density — so a `ControlSurface` reaches the phone field without this family reading anything.
91
+ - `tenancy` (does-not-apply) — sizes a tenancy summary row: a presentation part, for the same reason as `creator-studio` and `chat-people`.
92
+
93
+ ### Trigger composition
94
+
95
+ - `button` (does-not-apply) — `Button`’s own `asChild` renders the button AS an anchor or a router `Link` — it opens nothing and merges no open handler, so it is a different feature with the same name. It FORWARDS the trigger contract’s props when an anchored family clones it, which is the half that matters here.
96
+ - `theme` (does-not-apply) — `BloomColorScope`/`SeedScope` clone their child to merge a style instead of adding a layout node. No surface, no press, no open.
97
+
98
+ ### Controlled / uncontrolled state
99
+
100
+ - `textarea` (does-not-apply) — the VALUE belongs to React Native’s `TextInput`, which reconciles `value`/`defaultValue` itself; the only local state is the character count, which is why it is kept for the uncontrolled case alone.
101
+
102
+ ## Every family
103
+
104
+ | family | import | platform | doc | story | suites | Field membership | Control presentation | Trigger composition | Controlled / uncontrolled state |
105
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
106
+ | `accordion` | `@oxy.so/bloom/accordion` + barrel | universal | `accordion.mdx` | yes | 2 | | | | |
107
+ | `activity-heatmap` | `@oxy.so/bloom/activity-heatmap` + barrel | universal | `activity-heatmap.mdx` | yes | 1 | | | | |
108
+ | `admonition` | `@oxy.so/bloom/admonition` + barrel | universal | `admonition.mdx` | yes | 1 | | | | |
109
+ | `agent-chat` | `@oxy.so/bloom/agent-chat` + barrel | web fork | `agent-chat.mdx` | yes | 1 | n/a¹ | | | reads |
110
+ | `agent-limits-card` | `@oxy.so/bloom/agent-limits-card` + barrel | universal | `agent-limits-card.mdx` | yes | 1 | | | | reads |
111
+ | `agent-log` | `@oxy.so/bloom/agent-log` + barrel | universal | `agent-log.mdx` | yes | 1 | | | | |
112
+ | `agent-progress` | `@oxy.so/bloom/agent-progress` + barrel | universal | `agent-progress.mdx` | yes | 1 | | | | reads |
113
+ | `agent-thinking` | `@oxy.so/bloom/agent-thinking` + barrel | universal | `agent-thinking.mdx` | yes | 1 | | | | |
114
+ | `ai-chat` | `@oxy.so/bloom/ai-chat` + barrel | web fork | `ai-chat.mdx` | yes | 1 | | | | reads |
115
+ | `ai-profile-card` | `@oxy.so/bloom/ai-profile-card` + barrel | universal | `ai-profile-card.mdx` | yes | 1 | | | | reads |
116
+ | `alert-dialog` | `@oxy.so/bloom/alert-dialog` + barrel | web fork | `alert-dialog.mdx` | yes | 1 | | | | |
117
+ | `animated-check` | `@oxy.so/bloom/animated-check` + barrel | universal | `animated-check.mdx` | yes | 2 | | | | |
118
+ | `announcement` | `@oxy.so/bloom/announcement` + barrel | universal | `announcement.mdx` | yes | 1 | | | | |
119
+ | `app-shell` | `@oxy.so/bloom/app-shell` + barrel | universal | `app-shell.mdx` | yes | 1 | | | | reads |
120
+ | `aspect-ratio` | `@oxy.so/bloom/aspect-ratio` + barrel | universal | `aspect-ratio.mdx` | yes | 1 | | | | |
121
+ | `auth-card` | `@oxy.so/bloom/auth-card` + barrel | universal | `auth-card.mdx` | yes | 1 | | | | |
122
+ | `avatar` | `@oxy.so/bloom/avatar` + barrel | universal | `avatar.mdx` | yes | 5 | | | | |
123
+ | `avatar-group` | `@oxy.so/bloom/avatar-group` + barrel | web fork | `avatar-group.mdx` | yes | 3 | | | | |
124
+ | `badge` | `@oxy.so/bloom/badge` + barrel | universal | `badge.mdx` | yes | 1 | | | | |
125
+ | `booking` | `@oxy.so/bloom/booking` + barrel | universal | `booking.mdx` | yes | 3 | | | | reads |
126
+ | `bottom-sheet` | `@oxy.so/bloom/bottom-sheet` + barrel | web fork | `bottom-sheet.mdx` | yes | 6 | | | | |
127
+ | `breadcrumb` | `@oxy.so/bloom/breadcrumb` + barrel | universal | `breadcrumb.mdx` | yes | 1 | | | | |
128
+ | `button` | `@oxy.so/bloom/button` + barrel | web fork | `button.mdx` | yes | 48 | | | n/a¹ | |
129
+ | `button-group` | `@oxy.so/bloom/button-group` + barrel | universal | `button-group.mdx` | yes | 4 | | reads | | |
130
+ | `calendar` | `@oxy.so/bloom/calendar` + barrel | universal | `calendar.mdx` | yes | 1 | | | | reads |
131
+ | `call-ui` | `@oxy.so/bloom/call-ui` + barrel | universal | `call-ui.mdx` | yes | 1 | | | | |
132
+ | `card` | `@oxy.so/bloom/card` + barrel | universal | `card.mdx` | yes | 2 | | | | |
133
+ | `carousel` | `@oxy.so/bloom/carousel` + barrel | universal | `carousel.mdx` | yes | 1 | | | | |
134
+ | `category-bar` | `@oxy.so/bloom/category-bar` + barrel | universal | `category-bar.mdx` | yes | 2 | | | | |
135
+ | `chart-cards` | `@oxy.so/bloom/chart-cards` + barrel | universal | `chart-cards.mdx` | yes | 27 | | | | **adaptation** |
136
+ | `chat-composer` | `@oxy.so/bloom/chat-composer` + barrel | universal | `chat-composer.mdx` | yes | 2 | n/a¹ | n/a¹ | | reads |
137
+ | `chat-indicators` | `@oxy.so/bloom/chat-indicators` + barrel | universal | `chat-indicators.mdx` | yes | 1 | | | | |
138
+ | `chat-list` | `@oxy.so/bloom/chat-list` + barrel | universal | `chat-list.mdx` | yes | 2 | n/a¹ | | | **adaptation** |
139
+ | `chat-people` | `@oxy.so/bloom/chat-people` + barrel | universal | `chat-people.mdx` | yes | 1 | n/a¹ | n/a¹ | | |
140
+ | `chat-screen` | `@oxy.so/bloom/chat-screen` + barrel | universal | `chat-screen.mdx` | yes | 2 | | n/a¹ | | reads |
141
+ | `checkbox` | `@oxy.so/bloom/checkbox` + barrel | universal | `checkbox.mdx` | yes | 4 | reads | | | |
142
+ | `chip` | `@oxy.so/bloom/chip` + barrel | universal | `chip.mdx` | yes | 4 | | | | |
143
+ | `code` | `@oxy.so/bloom/code` + barrel | universal | `code.mdx` | yes | 2 | | | | |
144
+ | `command` | `@oxy.so/bloom/command` + barrel | web fork | `command.mdx` | yes | 1 | n/a¹ | | | reads |
145
+ | `composer-loader` | `@oxy.so/bloom/composer-loader` + barrel | web fork | `composer-loader.mdx` | yes | 1 | | | | |
146
+ | `composer-panel` | `@oxy.so/bloom/composer-panel` + barrel | web fork | `composer-panel.mdx` | yes | 1 | n/a¹ | | | reads |
147
+ | `composition-bar` | `@oxy.so/bloom/composition-bar` + barrel | universal | `composition-bar.mdx` | yes | 1 | | | | |
148
+ | `connection-dots` | `@oxy.so/bloom/connection-dots` + barrel | web fork | `connection-dots.mdx` | yes | 1 | | | | |
149
+ | `connection-status` | `@oxy.so/bloom/connection-status` + barrel | web fork | `connection-status.mdx` | yes | 1 | | | | |
150
+ | `content-panel` | `@oxy.so/bloom/content-panel` + barrel | web fork | `content-panel.mdx` | yes | 6 | | | | |
151
+ | `context-menu` | `@oxy.so/bloom/context-menu` + barrel | web fork | `context-menu.mdx` | yes | 2 | | | reads | |
152
+ | `control-surface` | `@oxy.so/bloom/control-surface` + barrel | universal | `composition.mdx` | yes | 3 | | reads | | |
153
+ | `creator-studio` | `@oxy.so/bloom/creator-studio` + barrel | universal | `creator-studio.mdx` | yes | 2 | | n/a¹ | | **adaptation** |
154
+ | `data-table` | `@oxy.so/bloom/data-table` + barrel | universal | `data-table.mdx` | yes | 3 | | | | reads |
155
+ | `date-picker` | `@oxy.so/bloom/date-picker` + barrel | universal | `date-picker.mdx` | yes | 4 | reads | reads | | reads |
156
+ | `design-tokens` | `@oxy.so/bloom/design-tokens` + barrel | universal | `design-tokens.mdx` | yes | 3 | | | | |
157
+ | `dialog` | `@oxy.so/bloom/dialog` + barrel | web fork | `dialog.mdx` | yes | 13 | | | | |
158
+ | `divider` | `@oxy.so/bloom/divider` + barrel | universal | `divider.mdx` | yes | 1 | | | | |
159
+ | `dot-grid-meter` | `@oxy.so/bloom/dot-grid-meter` + barrel | universal | `dot-grid-meter.mdx` | yes | 2 | | | | |
160
+ | `dropdown-menu` | `@oxy.so/bloom/dropdown-menu` + barrel | web fork | `dropdown-menu.mdx` | yes | 4 | | | reads | reads |
161
+ | `error-boundary` | `@oxy.so/bloom/error-boundary` + barrel | universal | `error-boundary.mdx` | yes | 1 | | | | |
162
+ | `eviction` | `@oxy.so/bloom/eviction` + barrel | universal | `eviction.mdx` | yes | 1 | | | | |
163
+ | `fab` | `@oxy.so/bloom/fab` + barrel | web fork | `fab.mdx` | yes | 4 | | | | |
164
+ | `field` | `@oxy.so/bloom/field` + barrel | universal | `field.mdx` | yes | 4 | reads | | | |
165
+ | `file-upload` | `@oxy.so/bloom/file-upload` + barrel | universal | `file-upload.mdx` | yes | 3 | reads | | | |
166
+ | `fill` | `@oxy.so/bloom/fill` + barrel | universal | `fill.mdx` | yes | 1 | | | | |
167
+ | `floating` | internal | universal | — | — | 11 | n/a¹ | | reads | reads |
168
+ | `fonts` | `@oxy.so/bloom/fonts` + barrel | web fork | `fonts.mdx` | yes | 2 | | | | |
169
+ | `frosted-icon-button` | `@oxy.so/bloom/frosted-icon-button` + barrel | web fork | `frosted-icon-button.mdx` | yes | 4 | | | | |
170
+ | `glass` | internal | universal | `glass.mdx` | — | 1 | | reads | | |
171
+ | `grid` | `@oxy.so/bloom/grid` + barrel | universal | `grid.mdx` | yes | 1 | | | | |
172
+ | `home-search` | `@oxy.so/bloom/home-search` + barrel | universal | `home-search.mdx` | yes | 1 | n/a¹ | | | |
173
+ | `hooks` | `@oxy.so/bloom/hooks` + barrel | universal | `hooks.mdx` | yes | 9 | | | | reads |
174
+ | `hover-card` | `@oxy.so/bloom/hover-card` + barrel | web fork | `hover-card.mdx` | yes | 2 | | | reads | reads |
175
+ | `icon-circle` | `@oxy.so/bloom/icon-circle` + barrel | universal | `icon-circle.mdx` | yes | 1 | | | | |
176
+ | `icons` | `@oxy.so/bloom/icons` + barrel | universal | `icons.mdx` | yes | 33 | | | | |
177
+ | `image-aspect-ratio-cache` | `@oxy.so/bloom/image-aspect-ratio-cache` + barrel | universal | `image-aspect-ratio-cache.mdx` | yes | 1 | | | | |
178
+ | `image-resolver` | `@oxy.so/bloom/image-resolver` + barrel | universal | `image-resolver.mdx` | yes | 7 | | | | |
179
+ | `important-alerts-card` | `@oxy.so/bloom/important-alerts-card` + barrel | universal | `important-alerts-card.mdx` | yes | 1 | | | | |
180
+ | `input-group` | `@oxy.so/bloom/input-group` + barrel | universal | `input-group.mdx` | yes | 3 | reads | | | |
181
+ | `input-otp` | `@oxy.so/bloom/input-otp` + barrel | universal | `input-otp.mdx` | yes | 2 | reads | | | **adaptation** |
182
+ | `item` | `@oxy.so/bloom/item` + barrel | universal | `item.mdx` | yes | 2 | | | | |
183
+ | `kbd` | `@oxy.so/bloom/kbd` + barrel | universal | `kbd.mdx` | yes | 1 | | | | |
184
+ | `label` | `@oxy.so/bloom/label` + barrel | universal | `label.mdx` | yes | 1 | | | | |
185
+ | `layout` | `@oxy.so/bloom/layout` + barrel | universal | `layout.mdx` | yes | 6 | | | | |
186
+ | `link-preview` | `@oxy.so/bloom/link-preview` + barrel | universal | `link-preview.mdx` | yes | 2 | | | | |
187
+ | `list` | `@oxy.so/bloom/list` + barrel | web fork | `list.mdx` | yes | 2 | | | | |
188
+ | `listing-actions` | `@oxy.so/bloom/listing-actions` + barrel | universal | `listing-actions.mdx` | yes | 2 | **adaptation** | | | reads |
189
+ | `listing-card` | `@oxy.so/bloom/listing-card` + barrel | universal | `listing-card.mdx` | yes | 3 | | | | |
190
+ | `listing-details` | `@oxy.so/bloom/listing-details` + barrel | universal | `listing-details.mdx` | yes | 4 | | | | reads |
191
+ | `listing-editor` | `@oxy.so/bloom/listing-editor` + barrel | universal | `listing-editor.mdx` | yes | 1 | **adaptation** | | | reads |
192
+ | `loading` | `@oxy.so/bloom/loading` + barrel | web fork | `loading.mdx` | yes | 2 | | | | |
193
+ | `lyrics` | `@oxy.so/bloom/lyrics` + barrel | universal | `lyrics.mdx` | yes | 2 | | | | |
194
+ | `map-marker` | `@oxy.so/bloom/map-marker` + barrel | universal | `map-marker.mdx` | yes | 2 | n/a¹ | | | |
195
+ | `media-card` | `@oxy.so/bloom/media-card` + barrel | universal | `media-card.mdx` | yes | 3 | | | | |
196
+ | `media-controls` | `@oxy.so/bloom/media-controls` + barrel | universal | `media-controls.mdx` | yes | 2 | n/a¹ | | | |
197
+ | `media-flight` | `@oxy.so/bloom/media-flight` | web fork | `media-flight.mdx` | yes | 3 | | | | |
198
+ | `media-header` | `@oxy.so/bloom/media-header` + barrel | universal | `media-header.mdx` | yes | 2 | | n/a¹ | | |
199
+ | `media-inset-border` | `@oxy.so/bloom/media-inset-border` + barrel | universal | `media-inset-border.mdx` | yes | 1 | | | | |
200
+ | `media-player` | `@oxy.so/bloom/media-player` + barrel | universal | `media-player.mdx` | yes | 3 | | | | |
201
+ | `media-shelf` | `@oxy.so/bloom/media-shelf` + barrel | universal | `media-shelf.mdx` | yes | 1 | | | | |
202
+ | `menubar` | `@oxy.so/bloom/menubar` + barrel | web fork | `menubar.mdx` | yes | 2 | | | reads | reads |
203
+ | `message-bubble` | `@oxy.so/bloom/message-bubble` + barrel | universal | `message-bubble.mdx` | yes | 1 | | | | |
204
+ | `message-media` | `@oxy.so/bloom/message-media` + barrel | universal | `message-media.mdx` | yes | 1 | n/a¹ | | | |
205
+ | `motion` | `@oxy.so/bloom/motion` + barrel | web fork | `motion.mdx` | yes | 2 | | | | |
206
+ | `music-library` | `@oxy.so/bloom/music-library` + barrel | universal | `music-library.mdx` | yes | 2 | n/a¹ | | | |
207
+ | `notification` | `@oxy.so/bloom/notification` + barrel | universal | `notification.mdx` | yes | 2 | | | | |
208
+ | `notification-center` | `@oxy.so/bloom/notification-center` + barrel | universal | `notification-center.mdx` | yes | 2 | | | | reads |
209
+ | `offering-badge` | `@oxy.so/bloom/offering-badge` + barrel | universal | `offering-badge.mdx` | yes | 3 | | | | |
210
+ | `overlay` | `@oxy.so/bloom/overlay` + barrel | universal | `overlay.mdx` | yes | 7 | | | | |
211
+ | `page-header` | `@oxy.so/bloom/page-header` + barrel | universal | `page-header.mdx` | yes | 2 | | reads | | |
212
+ | `pagination` | `@oxy.so/bloom/pagination` + barrel | universal | `pagination.mdx` | yes | 1 | | | | |
213
+ | `patient-info-card` | `@oxy.so/bloom/patient-info-card` + barrel | universal | `patient-info-card.mdx` | yes | 1 | | | | |
214
+ | `phone-input` | `@oxy.so/bloom/phone-input` | universal | `phone-input.mdx` | yes | 2 | reads | delegated | | reads |
215
+ | `place-reviews` | `@oxy.so/bloom/place-reviews` + barrel | universal | `place-reviews.mdx` | yes | 1 | | | | reads |
216
+ | `popover` | `@oxy.so/bloom/popover` + barrel | web fork | `popover.mdx` | yes | 6 | | | reads | reads |
217
+ | `portal` | `@oxy.so/bloom/portal` + barrel | web fork | `portal.mdx` | yes | 14 | | | | |
218
+ | `pressable-scale` | `@oxy.so/bloom/pressable-scale` + barrel | universal | `pressable-scale.mdx` | yes | 1 | | | | |
219
+ | `progressive-blur` | `@oxy.so/bloom/progressive-blur` + barrel | web fork | `progressive-blur.mdx` | yes | 1 | | | | |
220
+ | `property-insights` | `@oxy.so/bloom/property-insights` + barrel | universal | `property-insights.mdx` | yes | 3 | | | | reads |
221
+ | `provider` | `@oxy.so/bloom/provider` | universal | `provider.mdx` | yes | 1 | | | | |
222
+ | `questionnaire` | `@oxy.so/bloom/questionnaire` + barrel | universal | `questionnaire.mdx` | yes | 1 | **adaptation** | | | **adaptation** |
223
+ | `queue-panel` | `@oxy.so/bloom/queue-panel` + barrel | universal | `queue-panel.mdx` | yes | 1 | | | | **adaptation** |
224
+ | `radio` | `@oxy.so/bloom/radio` + barrel | universal | `radio.mdx` | yes | 3 | reads | | | |
225
+ | `radio-indicator` | `@oxy.so/bloom/radio-indicator` + barrel | universal | `radio-indicator.mdx` | yes | 1 | | | | |
226
+ | `rail` | `@oxy.so/bloom/rail` + barrel | universal | `rail.mdx` | yes | 1 | | | | |
227
+ | `rating` | `@oxy.so/bloom/rating` + barrel | universal | `rating.mdx` | yes | 3 | reads | | | |
228
+ | `recent-hires-card` | `@oxy.so/bloom/recent-hires-card` + barrel | universal | `recent-hires-card.mdx` | yes | 1 | | | | |
229
+ | `scroll` | `@oxy.so/bloom/scroll` + barrel | web fork | `scroll.mdx` | yes | 5 | | | | |
230
+ | `search` | `@oxy.so/bloom/search` + barrel | universal | `search.mdx` | yes | 1 | | | | |
231
+ | `segmented-control` | `@oxy.so/bloom/segmented-control` + barrel | universal | `segmented-control.mdx` | yes | 3 | reads | | | |
232
+ | `select` | `@oxy.so/bloom/select` + barrel | web fork | `select.mdx` | yes | 6 | reads | | reads | |
233
+ | `settings-list` | `@oxy.so/bloom/settings-list` + barrel | universal | `settings-list.mdx` | yes | 5 | | | | |
234
+ | `settings-modal` | `@oxy.so/bloom/settings-modal` + barrel | universal | `settings-modal.mdx` | yes | 1 | | | | reads |
235
+ | `sidebar` | `@oxy.so/bloom/sidebar` + barrel | universal | `sidebar.mdx` | yes | 1 | n/a¹ | | | reads |
236
+ | `skeleton` | `@oxy.so/bloom/skeleton` + barrel | universal | `skeleton.mdx` | yes | 1 | | | | |
237
+ | `slider` | `@oxy.so/bloom/slider` + barrel | universal | `slider.mdx` | yes | 4 | reads | | | |
238
+ | `social-button` | `@oxy.so/bloom/social-button` + barrel | universal | `social-button.mdx` | yes | 1 | | | | |
239
+ | `sortable-media` | `@oxy.so/bloom/sortable-media` + barrel | universal | `sortable-media.mdx` | yes | 1 | | | | |
240
+ | `stat-bar` | `@oxy.so/bloom/stat-bar` + barrel | universal | `stat-bar.mdx` | yes | 4 | | | | |
241
+ | `stat-cards` | `@oxy.so/bloom/stat-cards` + barrel | universal | `stat-cards.mdx` | yes | 4 | | | | |
242
+ | `stay-filters` | `@oxy.so/bloom/stay-filters` + barrel | universal | `stay-filters.mdx` | yes | 4 | n/a¹ | | | |
243
+ | `stay-search` | `@oxy.so/bloom/stay-search` + barrel | universal | `stay-search.mdx` | yes | 4 | | | | |
244
+ | `stepper` | `@oxy.so/bloom/stepper` + barrel | universal | `stepper.mdx` | yes | 3 | reads | | | |
245
+ | `styles` | `@oxy.so/bloom/styles` + barrel | universal | `styles.mdx` | yes | 42 | | | | |
246
+ | `subtle-hover` | `@oxy.so/bloom/subtle-hover` + barrel | universal | `subtle-hover.mdx` | yes | 1 | | | | |
247
+ | `surfaces` | `@oxy.so/bloom/surfaces` + barrel | web fork | `alert.mdx` | yes | 2 | | | | |
248
+ | `switch` | `@oxy.so/bloom/switch` + barrel | universal | `switch.mdx` | yes | 5 | reads | | | |
249
+ | `tab-bar` | `@oxy.so/bloom/tab-bar` | web fork | `tab-bar.mdx` | yes | 12 | | | reads | |
250
+ | `table` | `@oxy.so/bloom/table` + barrel | universal | `table.mdx` | yes | 1 | | | | |
251
+ | `tabs` | `@oxy.so/bloom/tabs` + barrel | universal | `tabs.mdx` | yes | 2 | | | | |
252
+ | `task-list` | `@oxy.so/bloom/task-list` + barrel | universal | `task-list.mdx` | yes | 1 | | | | |
253
+ | `teleport` | `@oxy.so/bloom/teleport` | universal | `teleport.mdx` | yes | 1 | | | | |
254
+ | `tenancy` | `@oxy.so/bloom/tenancy` + barrel | universal | `tenancy.mdx` | yes | 3 | | n/a¹ | | |
255
+ | `text-field` | `@oxy.so/bloom/text-field` + barrel | universal | `text-field.mdx` | yes | 10 | reads | reads | | |
256
+ | `textarea` | `@oxy.so/bloom/textarea` + barrel | universal | `textarea.mdx` | yes | 4 | reads | reads | | n/a¹ |
257
+ | `theme` | `@oxy.so/bloom/theme` + barrel | web fork | `theme.mdx` | yes | 242 | | | n/a¹ | reads |
258
+ | `theme-toggle` | `@oxy.so/bloom/theme-toggle` + barrel | universal | `theme-toggle.mdx` | yes | 1 | n/a¹ | | | |
259
+ | `toast` | `@oxy.so/bloom/toast` + barrel | universal | `toast.mdx` | yes | 17 | | | | |
260
+ | `tooltip` | `@oxy.so/bloom/tooltip` + barrel | web fork | `tooltip.mdx` | yes | 1 | | | | |
261
+ | `track-list` | `@oxy.so/bloom/track-list` + barrel | universal | `track-list.mdx` | yes | 1 | | | | **adaptation** |
262
+ | `typography` | `@oxy.so/bloom/typography` + barrel | universal | `typography.mdx` | yes | 7 | | | | |
263
+ | `user-hover-card` | `@oxy.so/bloom/user-hover-card` + barrel | universal | `user-hover-card.mdx` | yes | 4 | | | | |
264
+ | `web-search` | `@oxy.so/bloom/web-search` + barrel | universal | `web-search.mdx` | yes | 1 | | | | |
265
+ | `wizard` | `@oxy.so/bloom/wizard` + barrel | universal | `wizard.mdx` | yes | 1 | | | | |
266
+ | `zoomable-media-gallery` | `@oxy.so/bloom/zoomable-media-gallery` | universal | `zoomable-media-gallery.mdx` | yes | 2 | | | | |
package/docs/checkbox.mdx CHANGED
@@ -85,6 +85,22 @@ import { CheckboxCard } from '@oxy.so/bloom/checkbox';
85
85
 
86
86
  `title` is the accessible name and `description` the hint.
87
87
 
88
+ ## In a `Field`
89
+
90
+ A checkbox inside a `Field` takes the field's id, its described-by, `aria-invalid`
91
+ and `disabled` — and **keeps its own label as its name**, because the words beside
92
+ the box are the ones the user reads as the control. The field's label names the
93
+ FIELD:
94
+
95
+ ```tsx
96
+ <Field label="Terms" error={errors.terms}>
97
+ <Checkbox label="I accept the terms" checked={ok} onCheckedChange={setOk} />
98
+ </Field>
99
+ ```
100
+
101
+ A checkbox with no label of its own falls back to the field's. Several checkboxes
102
+ in one field is the `multiple` case — see [Field](/docs/field).
103
+
88
104
  ## Touch target
89
105
 
90
106
  The box is 16dp at the default size, well under what a finger needs. The pressable is grown to 44dp with `hitSlop` — the target grows, the drawing does not — and the slack is derived per size, so `large` does not overshoot into a neighbouring row.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Composition
3
- description: How Bloom components work together — the inherited presentation contract, field membership, trigger composition, scroll ownership and edge claims — with what is done and what is not.
3
+ description: How Bloom components work together — the inherited presentation contract, field membership, trigger composition, scroll ownership, edge claims and screen scope — with the adoption measured per family.
4
4
  order: 3
5
5
  ---
6
6
 
@@ -47,7 +47,21 @@ It renders no node of its own, so it can be dropped anywhere without moving anyt
47
47
 
48
48
  A field left `undefined` inherits rather than resetting: `<ControlSurface density="small">` inside a glass island stays glass.
49
49
 
50
- **Who reads it today:** `ButtonGroup` and `ButtonGroupItem`. `Button` does not — a `Button` in a floating header keeps its own chrome, which is right for a primary action in its own capsule and wrong for an icon action that should sit flush on an island. Use `ButtonGroup`/`ButtonGroupItem` for the latter. Extending this to more families is adoption work, not a gap in the contract; see [What is not done](#what-is-not-done).
50
+ **Who reads it today:** `ButtonGroup` and `ButtonGroupItem` read `material`; `TextField`, `Textarea` and `TimeField` read `density`, and `PhoneInput` gets it by passing its `size` straight to `TextField`. `density` is `'medium' | 'small'`, which is exactly the text-field family's `size` vocabulary — so a filter row can ask for `small` once instead of writing it on five controls:
51
+
52
+ ```tsx
53
+ <ControlSurface density="small">
54
+ <TextFieldInput label="Search" value={q} onChangeText={setQ} />
55
+ <Textarea label="Notes" />
56
+ <TimeField accessibilityLabel="From" value={from} onChange={setFrom} />
57
+ </ControlSurface>
58
+ ```
59
+
60
+ A family whose sizes are a THREE-rung scale of its own (`small | medium | large`) is deliberately not a subject: a container setting `density` could not say which rung it meant.
61
+
62
+ **`Button` does not read it** — a `Button` in a floating header keeps its own chrome, which is right for a primary action in its own capsule and wrong for an icon action that should sit flush on an island. Use `ButtonGroup`/`ButtonGroupItem` for the latter.
63
+
64
+ Which families read it, which are exempt and why: [Adoption matrix](/docs/adoption-matrix).
51
65
 
52
66
  Precedence is gated in both directions by `src/__tests__/ControlSurface.test.tsx`, including the assertion that the context carries exactly two fields.
53
67
 
@@ -69,22 +83,41 @@ A `Field` used to draw a label, a control and a hint, and wire none of them toge
69
83
  - **`multiple` is the several-controls case** — a radio set, a date's three boxes. The field becomes a labelled `group`, publishes no control id and describes the GROUP rather than each control: one id on three inputs is invalid HTML, and one error described by three inputs is announced three times.
70
84
  - **A floating label and a `Field` label are alternatives, not layers.** `<TextFieldInput floatingLabel>` inside a `Field label="…"` shows the same words above the box and inside it. Pick one.
71
85
 
72
- A control opts in by reading `useFieldControl()`:
86
+ A control opts in with `useFieldMembership()`, which resolves the four decisions in one place:
73
87
 
74
88
  ```tsx
75
- const field = useFieldControl();
76
- <TextInput
77
- nativeID={field?.controlId ?? nativeID}
78
- aria-describedby={field?.describedBy}
79
- aria-invalid={field?.invalid || isInvalid || undefined}
80
- accessibilityLabel={field?.labelText ?? label}
81
- editable={!(field?.disabled || disabled)}
89
+ const field = useFieldMembership({ accessibilityLabel, label, disabled, invalid: isInvalid });
90
+
91
+ <Pressable
92
+ nativeID={field.nativeID}
93
+ accessibilityLabel={field.accessibilityLabel}
94
+ aria-describedby={field.describedBy}
95
+ aria-invalid={field.invalid || undefined}
96
+ disabled={field.disabled}
82
97
  />
83
98
  ```
84
99
 
100
+ It is a function rather than a convention because each of the four has a direction that is silently wrong the other way round:
101
+
102
+ | what | direction | why |
103
+ | --- | --- | --- |
104
+ | the NAME | `??` — the field's, then the control's | a switch or a slider draws no words, so the field's label is the only name available |
105
+ | `disabled`, `invalid` | `||` — both | they RESTRICT; a control that could re-enable itself inside a disabled field is the bug |
106
+ | the ID | the caller's, then the field's | an app that wired its own association keeps it |
107
+ | `aria-describedby` | JOINED | it is an id LIST, so the field's error is added to the control's own hint rather than replacing it |
108
+
109
+ **`labelPlacement` decides which label wins.** A label drawn ABOVE the control (`TextField`, `Textarea`) is the same thing the field's label says, so the field wins. A label BESIDE the control (a checkbox's caption, a radio's option text) is the words the user reads AS the control, so it keeps its own and the field only fills a gap — otherwise a screen reader says one thing while the eye reads another.
110
+
85
111
  Opt-in, not magic. A child that does not read the context renders exactly as before — a container cannot give an arbitrary child behaviour by wrapping it, and one that inspected its children's types to try would be guessing.
86
112
 
87
- **Who reads it today:** `TextFieldInput` (both the default and floating-label renderings). Gate: `src/__tests__/FieldAssociation.test.tsx`.
113
+ **Who reads it today:** `Checkbox`, `FileUpload`, `InputGroup`, `InputOtp`, `PhoneInput`, `Radio`, `RadioGroup`, `RatingInput`, `SegmentedControl`, `Select` (its trigger), `Slider`, `Stepper`, `Switch`, `Textarea`, `TextField`/`TextFieldInput` and `TimeField`.
114
+
115
+ Two consequences worth knowing:
116
+
117
+ - **A control that cannot name itself no longer needs a name prop inside a field.** `accessibilityLabel` on `Stepper`, `RatingInput` and `TimeField`, and `label` on `RadioGroup`, `SegmentedControl` and `SelectTrigger`, were required props and are now optional — with neither the prop nor a field the control announces nothing and warns once in development.
118
+ - **A composite hands the field's state to the halves that cannot read it themselves.** `PhoneInput`'s country picker is the measured case: the number input read the field, the picker did not, so it stayed operable inside a disabled field.
119
+
120
+ Gates: `src/__tests__/FieldAssociation.test.tsx` (the field publishes) and `src/__tests__/FieldMembership.test.tsx` (fifteen families apply, in both directions, and the subject list is asserted against the matrix).
88
121
 
89
122
  ## Trigger composition — `asChild`
90
123
 
@@ -107,6 +140,8 @@ Every anchored family (`Popover`, `DropdownMenu`, `ContextMenu`, `Menubar`, `Sel
107
140
  </ButtonGroup>
108
141
  ```
109
142
 
143
+ **A trigger that IS a form control carries the field's wiring too** — `nativeID`, `aria-describedby` and `aria-invalid` are part of the handle, and `SelectTrigger` passes them. The other four families deliberately do not: a `Popover` inside a `Field` is not that field's control and must not take its id, so `TriggerSlot` reads no field context itself and each family decides whether it is the control.
144
+
110
145
  Gate: `src/__tests__/TriggerComposition.test.tsx`.
111
146
 
112
147
  ## Scroll ownership — `ScrollOffsetProvider`
@@ -144,16 +179,53 @@ The bottom edge is the tab bar and the FAB (`docs/fab.mdx`). The top edge is chr
144
179
  <ScrollView contentContainerStyle={{ paddingTop: useTopEdgeInset() }}>
145
180
  ```
146
181
 
147
- `BloomProvider` mounts both. A nested provider scopes a subtree to its own claims — a full-screen modal with its own header, whose content must clear the MODAL's header rather than the screen's.
182
+ `BloomProvider` mounts both. A nested provider scopes a subtree to its own claims — which is what the next section is.
148
183
 
149
184
  A claim registers in an effect, so a reader mounted in the same commit sees `0` on its first render. `PageHeader` narrows that to one frame by claiming its resting height (safe area + 56) immediately and replacing it with the measured height when `onLayout` reports.
150
185
 
186
+ ## Screen scope — `ScreenScope`
187
+
188
+ `@oxy.so/bloom/layout`
189
+
190
+ A modal, a bottom sheet or a detail panel is a second SCREEN drawn over the first, and two of the contracts above are per-screen by design. Without a boundary they reach across it, in both directions and with no error either way:
191
+
192
+ - A header inside a full-screen dialog **claims the top edge of the page underneath it**. Claims combine with `max`, so the page behind the modal is padded for chrome it does not have — and the padding appears and disappears with the modal, because the claim unregisters on unmount.
193
+ - Chrome inside the sheet **reads the page's scroll offset** and follows a scroller the user is not touching. The sheet's own list scrolls and its header does not react; the page behind scrolls and it does.
194
+
195
+ `ScreenScope` is the boundary: a fresh top-edge registry, a fresh bottom-edge registry and a `null` scroll offset, which is the honest value — nothing inside has said which scroller it owns yet. A scroll owner INSIDE publishes in the ordinary way and wins, because it is nearer.
196
+
197
+ ```tsx
198
+ <ScreenScope>
199
+ <PageHeader title="Edit listing" onBack={close} />
200
+ <ScrollView contentContainerStyle={{ paddingTop: useTopEdgeInset() }}>…</ScrollView>
201
+ </ScreenScope>
202
+ ```
203
+
204
+ The bottom edge is scoped for the mirror reason: the bottom edge is the tab bar and the FAB, and a sheet is drawn OVER both, so a form inside it that cleared the tab bar would end in a band of empty space.
205
+
206
+ **Bloom's own surfaces mount it** — `DialogBody` (so every `Dialog` placement, including the bottom-sheet one, gets it) and `BottomSheetBase`. Mount it yourself only around a surface Bloom does not own: an app's own full-screen route presented over another.
207
+
208
+ Gate: `src/__tests__/ScreenScope.test.tsx`, asserting both directions — a claim inside must not escape, and the surface's own chrome must still be readable inside.
209
+
210
+ ## Five compositions that are not a header
211
+
212
+ The contracts are worth nothing if they only work in the screen they were built for, so these five are gated together in `src/__tests__/composition-integration.test.tsx`:
213
+
214
+ 1. **A form in a bottom sheet** — the field's error reaches the input across the sheet boundary, `Field disabled` reaches the input, the select trigger AND the switch, the switch takes its name from the field, and submit fires once per press.
215
+ 2. **An interactive card with a secondary action** — pressing "Archive" inside a pressable row does not also open the row. (React Native does not bubble a press; a web build does bubble a click, so this half is a browser check, not this one.)
216
+ 3. **A chat composer** — two presses send twice, and a disabled bar fires nothing at all.
217
+ 4. **A data screen** — a table inside a panel inside a card keeps its own accessible name and renders its empty state; a filter in a `ControlSurface density="small"` draws the small geometry rather than merely receiving a prop.
218
+ 5. **A screen with overlaying chrome and a modal over it** — the page pads for its own chrome, the modal's chrome does not reach it, and a sheet on that screen clears nothing of the page.
219
+
220
+ What that instrument cannot see is written at the top of the file: no paint, no layer order, no hit testing, no focus. Those have their own scripts in `scripts/verify-*.mjs`.
221
+
151
222
  ## What is not done
152
223
 
153
- This page describes contracts that exist and are gated. Issue #145 asks for more than that, and the rest is honest to name:
224
+ This page describes contracts that exist and are gated, and the [adoption matrix](/docs/adoption-matrix) measures where they are read. What is still open, named rather than implied:
154
225
 
155
- - **No full inventory of the public API.** The matrix over every family — platforms, contexts, state families, triggers, semantics, styles, tests — has not been produced. Nothing here should be read as "the library was audited"; it was extended where the header integration needed it.
156
- - **State-family naming is not unified.** `value`/`defaultValue`/`onValueChange`, `checked`/`onCheckedChange` and `open`/`onOpenChange` are used consistently by the families that have them, and `useControllableState` is the shared implementation, but no sweep has been made to rename the exceptions (`onChangeText` among them, which is a React Native contract worth keeping).
157
- - **Only two families read `ControlSurface`, and one reads `Field`.** Every other control renders as it always did. Adoption is per-family work with its own regressions to check.
226
+ - **Three families hand-roll the semantics of a grouped choice.** `questionnaire`, `listing-editor` and `listing-actions` write `role="radiogroup"`/`role="checkbox"` over their own cards instead of composing `RadioGroup`/`Checkbox`, so a `Field` can neither name nor disable them. Each is a redesign of an answer control rather than an import, which is why it is recorded instead of rushed. The matrix names them and the gate keeps the list from growing.
227
+ - **Seven families reconcile a controlled/uncontrolled pair by hand** rather than through `useControllableState`. The matrix names them too; `input-otp` has a reason (it cleans and pads the string), the rest do not.
228
+ - **State-family naming is not unified.** `value`/`defaultValue`/`onValueChange`, `checked`/`onCheckedChange` and `open`/`onOpenChange` are used consistently by the families that have them, but no sweep has been made to rename the exceptions (`onChangeText` among them, which is a React Native contract worth keeping).
158
229
  - **`BloomProvider` still imports the Expo Router adapter directly.** Decoupling the core from the router is listed in #145 §8 and is not in this change.
159
230
  - **No adaptive overflow.** Actions that do not fit are not collapsed into a menu. #145 says explicitly that this is not required for the first header, and nothing here does it.
231
+ - **iOS and Android are not device-verified for any of this.** The instruments used are `tsc`, the jest suites and a real Chrome; a native run is not among them.
@@ -159,3 +159,14 @@ They are a convenience **on a text box**, not a `spinbutton`: the control
159
159
  announces itself as the text field it is, named by `accessibilityLabel`, and
160
160
  every value the arrows can reach can also be typed. A `spinbutton` role would
161
161
  promise `aria-valuenow` over a value that is a string and may be empty.
162
+
163
+ `TimeField` reads the same two contracts the text field does: `size` comes from a
164
+ `ControlSurface density` when it is not written, and an enclosing `Field` supplies
165
+ the accessible name (`"--:--"` names nothing, which is why the name used to be a
166
+ required prop), the id, the described-by, `aria-invalid` and `disabled`.
167
+
168
+ ```tsx
169
+ <Field label="Check-in" error={errors.checkIn}>
170
+ <TimeField value={checkIn} onChange={setCheckIn} />
171
+ </Field>
172
+ ```
package/docs/field.mdx CHANGED
@@ -40,7 +40,7 @@ import { TextField, TextFieldInput } from '@oxy.so/bloom/text-field';
40
40
 
41
41
  ## What the wiring actually does
42
42
 
43
- The label, description and error are ASSOCIATED with the control, not merely stacked around it. A control opts in by reading `useFieldControl()`; Bloom's `TextFieldInput` does.
43
+ The label, description and error are ASSOCIATED with the control, not merely stacked around it. A control opts in by reading `useFieldMembership()`, and fifteen Bloom families do.
44
44
 
45
45
  | the field publishes | the control applies |
46
46
  | --- | --- |
@@ -51,14 +51,40 @@ The label, description and error are ASSOCIATED with the control, not merely sta
51
51
  | `required` | `aria-required` |
52
52
  | `disabled` | its own disabled state |
53
53
 
54
- Three things worth knowing:
54
+ Four things worth knowing:
55
55
 
56
56
  - **The ids come from `useId`** when no `nativeID` is given, so they survive SSR and hydration. An explicit `nativeID` still owns every derived id (`<id>-description`, `<id>-error`), so a form already wired by hand keeps working.
57
- - **The field's label wins over the control's own.** On web `aria-label` OVERRIDES a `<label for>`, and on native there is no association at all and the control must carry the name — so a control inside a field defers to the field's label text, and the visible label and the announced name cannot disagree.
58
- - **`disabled` is a constraint, not a default.** A control inside a disabled field is disabled and cannot opt out.
57
+ - **The field's label wins over a label the control draws ABOVE itself.** On web `aria-label` OVERRIDES a `<label for>`, and on native there is no association at all and the control must carry the name — so a `TextFieldInput` or a `Textarea` inside a field defers to the field's label text, and the visible label and the announced name cannot disagree.
58
+ - **A label the control draws BESIDE itself keeps winning.** A checkbox's caption and a radio's option text are the words the user reads as the control; the field's label names the FIELD. Giving the field's label to a checkbox that draws "Accept the terms" would announce one thing while the screen says another.
59
+ - **`disabled` is a constraint, not a default.** A control inside a disabled field is disabled and cannot opt out — including the halves a composite owns, like `PhoneInput`'s country picker or an `InputGroup`'s addon button.
59
60
 
60
61
  Opt-in, not magic: a child that does not read the context renders exactly as before. A container cannot give an arbitrary child behaviour by wrapping it, and one that inspected its children's types to try would be guessing. See [Composition](/docs/composition).
61
62
 
63
+ ## Which controls read it
64
+
65
+ `Checkbox`, `FileUpload`, `InputGroup`, `InputOtp`, `PhoneInput`, `Radio`,
66
+ `RadioGroup`, `RatingInput`, `SegmentedControl`, `SelectTrigger`, `Slider`,
67
+ `Stepper`, `Switch`, `Textarea`, `TextField`/`TextFieldInput`, `TimeField`.
68
+
69
+ The practical consequence is that a control which draws no words of its own needs
70
+ no name prop inside a field:
71
+
72
+ ```tsx
73
+ <Field label="Notify members">
74
+ <Switch value={notify} onValueChange={setNotify} />
75
+ </Field>
76
+ ```
77
+
78
+ That switch announces "Notify members, switch, on". Outside a field it would
79
+ announce "switch, on" and warn once in development, which is why
80
+ `accessibilityLabel` is still the prop to reach for when there is no field.
81
+ `label` on `RadioGroup`, `SegmentedControl` and `SelectTrigger`, and
82
+ `accessibilityLabel` on `Stepper`, `RatingInput` and `TimeField`, were REQUIRED
83
+ props before this and are optional now for the same reason.
84
+
85
+ Which families are outside the contract, and why each one is: the
86
+ [adoption matrix](/docs/adoption-matrix).
87
+
62
88
  ## One control, or several
63
89
 
64
90
  A field with ONE control gives it the field's id and describes the error BY that control. A field with several — a radio set, a date's three boxes — must not: three inputs sharing one id is invalid HTML, and one error described by three inputs is announced three times.
@@ -111,6 +137,11 @@ up to the pixel with a `TextField` built from `TextFieldLabel` / `TextFieldHint`
111
137
  `SegmentedControl`, a custom control. It supplies the label/description/error
112
138
  and the id wiring; the control supplies its own role and state.
113
139
 
140
+ The rule a control applies is one function, `useFieldMembership()` — exported
141
+ from `@oxy.so/bloom/field` for an app's own control, so it resolves membership
142
+ the way Bloom's fifteen do rather than by reimplementing four `??`/`||`
143
+ decisions. [Composition](/docs/composition) has the table of directions.
144
+
114
145
  For the label alone, without the surrounding structure, use
115
146
  [`Label`](/docs/bloom/main/label). For a control with attached buttons or
116
147
  prefixes, use [`InputGroup`](/docs/bloom/main/input-group) *inside* a `Field`.
@@ -77,6 +77,19 @@ The "Upload file" / "Upload in progress" frames, pixel-matched on web.
77
77
 
78
78
  Motion: fills and borders ease over 300ms, the ring's dash over 200ms linear, the pill over 160/300ms. Content swaps use a staggered reveal — each line rises 12px out of a 3px blur over 500ms (`cubic-bezier(0.22, 1, 0.36, 1)`), 40ms apart, and leaves with a 200ms fade. Web runs all of it as self-injected CSS on `dataSet` hooks; native drives opacity and translate with `Animated` **without the blur** (no portable blur filter). Reduced motion removes every transition.
79
79
 
80
+ ## In a `Field`
81
+
82
+ The drop zone names itself with `accessibilityLabel`, or with the field's label
83
+ when there is one — `'Upload a file'` is the last resort. The field's `disabled`
84
+ blocks the picker whatever the caller passed, and its error is described BY the
85
+ zone:
86
+
87
+ ```tsx
88
+ <Field label="Lease agreement" error={errors.lease} description="PDF, up to 10 MB">
89
+ <FileUpload onPickFiles={pick} />
90
+ </Field>
91
+ ```
92
+
80
93
  ## Hazards
81
94
 
82
95
  - **The progress dash is computed in real units, rather than a normalized `pathLength={100}`** — react-native-svg does not honour `pathLength` everywhere, so the length of the rounded-rect path is computed from the measured box instead. Change the inset or radius in one place only (`RING_INSET` / `RING_RADIUS`).
package/docs/glass.mdx CHANGED
@@ -138,3 +138,18 @@ Gate: `src/theme/__tests__/chrome-glass.test.ts`, which pins the two floors to t
138
138
  An island also mounts a `ControlSurface` with `material: 'glass'`, so controls inside it paint flush. That is the material's rule rather than a preference: a translucent pane inside a translucent pane composites two alphas and two blurs for one pane's worth of depth, and on Android the second blur is not available at all.
139
139
 
140
140
  Internal (`glass/` is not a published subpath). It is reached through `ButtonGroup`'s `glass` variant and `PageHeader`'s floating presentation.
141
+
142
+ ### On a device: tint, and no blur
143
+
144
+ Measured on a **Pixel 8a** over a hard 10dp stripe pattern, sampling the same rows inside a chrome island and on the bare stripes beside it:
145
+
146
+ | backdrop stripe | bare | inside the island |
147
+ | --- | --- | --- |
148
+ | white | 255 | 254 |
149
+ | black | 0 | 210 |
150
+
151
+ The backdrop contributes **44 of 255** — the pane is translucent — and the stripe **edge stays sharp**, where a blur would smear a 26px transition. That is the Android behaviour this file describes: `expo-blur` is asked for no blur method, so it paints its tint and nothing else. Nothing in `logcat` asked for one or reported a crash.
152
+
153
+ The composite works out at ≈**0.83** opacity against the web pane's 0.72 plus sheen. The difference is `expo-blur`'s own tint — one `intensity` drives it together with the radius and a caller cannot decline it — which is the platform gap `GLASS_BLUR_INTENSITY` documents, appearing exactly where it said it would.
154
+
155
+ The ramp `PageHeader` draws over the same material was checked for the `stopColor` alpha trap at the same time: **0 achromatic rows of 311** in both modes, so the hue survives the SVG renderer.
@@ -71,6 +71,28 @@ field, which belongs to [`Field`](/docs/bloom/main/field) or
71
71
  `InputGroup` goes *inside* a `Field` — the group is the control, the field is
72
72
  its label, hint and error.
73
73
 
74
+ ## In a `Field`
75
+
76
+ A group holding a control and its addons is one control as far as a `Field` is
77
+ concerned: the field's `disabled` and invalid state reach every member through the
78
+ group — **including an addon's `Button`**, which reads neither the field nor the
79
+ text-field group itself.
80
+
81
+ ```tsx
82
+ <Field label="Domain" error={errors.domain} disabled={locked}>
83
+ <InputGroup>
84
+ <InputGroupAddon>https://</InputGroupAddon>
85
+ <TextFieldInput value={domain} onChangeText={setDomain} />
86
+ <InputGroupAddon divider>
87
+ <Button size="small" variant="ghost" onPress={check}>Check</Button>
88
+ </InputGroupAddon>
89
+ </InputGroup>
90
+ </Field>
91
+ ```
92
+
93
+ The group publishes no NAME: it does not know which of its children is the control
94
+ the label points at, so the id stays the field's and the input's to apply.
95
+
74
96
  ## `divider`
75
97
 
76
98
  Use it when the addon is a distinct target — a button, a dropdown. Skip it for