@hzblj/kara-components 0.9.0 → 0.11.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 (281) hide show
  1. package/CHANGELOG.md +72 -28
  2. package/README.md +8 -8
  3. package/lib/module/atoms/activity-indicator/activity-indicator.android.js +6 -1
  4. package/lib/module/atoms/activity-indicator/activity-indicator.android.js.map +1 -1
  5. package/lib/module/atoms/activity-indicator/activity-indicator.ios.js +2 -0
  6. package/lib/module/atoms/activity-indicator/activity-indicator.ios.js.map +1 -1
  7. package/lib/module/atoms/avatar/avatar.js +5 -0
  8. package/lib/module/atoms/avatar/avatar.js.map +1 -1
  9. package/lib/module/atoms/button/button-tokens.js +2 -2
  10. package/lib/module/atoms/button/button-tokens.js.map +1 -1
  11. package/lib/module/atoms/button/button.android.js +6 -2
  12. package/lib/module/atoms/button/button.android.js.map +1 -1
  13. package/lib/module/atoms/button/button.ios.js +7 -2
  14. package/lib/module/atoms/button/button.ios.js.map +1 -1
  15. package/lib/module/atoms/card/card.android.js +5 -3
  16. package/lib/module/atoms/card/card.android.js.map +1 -1
  17. package/lib/module/atoms/card/card.ios.js +7 -3
  18. package/lib/module/atoms/card/card.ios.js.map +1 -1
  19. package/lib/module/atoms/checkbox/checkbox.android.js +1 -1
  20. package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
  21. package/lib/module/atoms/color-picker/color-picker.android.js +32 -5
  22. package/lib/module/atoms/color-picker/color-picker.android.js.map +1 -1
  23. package/lib/module/atoms/fab/fab.android.js +9 -1
  24. package/lib/module/atoms/fab/fab.android.js.map +1 -1
  25. package/lib/module/atoms/fab/fab.ios.js +9 -3
  26. package/lib/module/atoms/fab/fab.ios.js.map +1 -1
  27. package/lib/module/atoms/glass-view/glass-view.android.js +1 -1
  28. package/lib/module/atoms/glass-view/glass-view.android.js.map +1 -1
  29. package/lib/module/atoms/h-stack/h-stack.ios.js +5 -3
  30. package/lib/module/atoms/h-stack/h-stack.ios.js.map +1 -1
  31. package/lib/module/atoms/icon-button/icon-button.android.js +9 -0
  32. package/lib/module/atoms/icon-button/icon-button.android.js.map +1 -1
  33. package/lib/module/atoms/icon-button/icon-button.ios.js +26 -6
  34. package/lib/module/atoms/icon-button/icon-button.ios.js.map +1 -1
  35. package/lib/module/atoms/image/image.js +4 -4
  36. package/lib/module/atoms/image/image.js.map +1 -1
  37. package/lib/module/atoms/pressable/pressable.js +52 -28
  38. package/lib/module/atoms/pressable/pressable.js.map +1 -1
  39. package/lib/module/atoms/progress/progress.android.js +7 -0
  40. package/lib/module/atoms/progress/progress.android.js.map +1 -1
  41. package/lib/module/atoms/progress/progress.ios.js +6 -1
  42. package/lib/module/atoms/progress/progress.ios.js.map +1 -1
  43. package/lib/module/atoms/segmented-control/segmented-control.android.js +1 -1
  44. package/lib/module/atoms/segmented-control/segmented-control.android.js.map +1 -1
  45. package/lib/module/atoms/segmented-control/segmented-control.ios.js +1 -1
  46. package/lib/module/atoms/segmented-control/segmented-control.ios.js.map +1 -1
  47. package/lib/module/atoms/skeleton/skeleton.js +20 -6
  48. package/lib/module/atoms/skeleton/skeleton.js.map +1 -1
  49. package/lib/module/atoms/surface/surface.android.js +5 -3
  50. package/lib/module/atoms/surface/surface.android.js.map +1 -1
  51. package/lib/module/atoms/surface/surface.ios.js +7 -3
  52. package/lib/module/atoms/surface/surface.ios.js.map +1 -1
  53. package/lib/module/atoms/switch/switch.android.js +1 -1
  54. package/lib/module/atoms/switch/switch.android.js.map +1 -1
  55. package/lib/module/atoms/text/text.android.js +5 -3
  56. package/lib/module/atoms/text/text.android.js.map +1 -1
  57. package/lib/module/atoms/text/text.ios.js +6 -4
  58. package/lib/module/atoms/text/text.ios.js.map +1 -1
  59. package/lib/module/atoms/v-stack/v-stack.ios.js +5 -3
  60. package/lib/module/atoms/v-stack/v-stack.ios.js.map +1 -1
  61. package/lib/module/atoms/z-stack/z-stack.ios.js +5 -3
  62. package/lib/module/atoms/z-stack/z-stack.ios.js.map +1 -1
  63. package/lib/module/glyph/glyph.android.js +2 -0
  64. package/lib/module/glyph/glyph.android.js.map +1 -1
  65. package/lib/module/glyph/glyph.ios.js +5 -2
  66. package/lib/module/glyph/glyph.ios.js.map +1 -1
  67. package/lib/module/molecules/collapsible/collapsible.android.js +18 -8
  68. package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
  69. package/lib/module/molecules/labeled-content/labeled-content.js +4 -1
  70. package/lib/module/molecules/labeled-content/labeled-content.js.map +1 -1
  71. package/lib/module/molecules/link/link.js +4 -1
  72. package/lib/module/molecules/link/link.js.map +1 -1
  73. package/lib/module/molecules/list-item/list-item.js +4 -1
  74. package/lib/module/molecules/list-item/list-item.js.map +1 -1
  75. package/lib/module/molecules/navigation-bar/navigation-bar.android.js +1 -1
  76. package/lib/module/molecules/navigation-bar/navigation-bar.android.js.map +1 -1
  77. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js +1 -1
  78. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js.map +1 -1
  79. package/lib/module/molecules/rn-list-item/rn-list-item.js +10 -2
  80. package/lib/module/molecules/rn-list-item/rn-list-item.js.map +1 -1
  81. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js +3 -4
  82. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js.map +1 -1
  83. package/lib/module/molecules/stepper/stepper.ios.js +6 -2
  84. package/lib/module/molecules/stepper/stepper.ios.js.map +1 -1
  85. package/lib/module/motion.js +19 -0
  86. package/lib/module/motion.js.map +1 -0
  87. package/lib/module/organisms/fab-menu/fab-menu.android.js +8 -5
  88. package/lib/module/organisms/fab-menu/fab-menu.android.js.map +1 -1
  89. package/lib/module/organisms/fab-menu/fab-menu.ios.js +5 -2
  90. package/lib/module/organisms/fab-menu/fab-menu.ios.js.map +1 -1
  91. package/lib/module/organisms/snackbar/snackbar.js +142 -17
  92. package/lib/module/organisms/snackbar/snackbar.js.map +1 -1
  93. package/lib/module/organisms/swipe-actions/swipe-actions.android.js +1 -1
  94. package/lib/module/organisms/swipe-actions/swipe-actions.android.js.map +1 -1
  95. package/lib/module/use-stack-modifiers.js +10 -8
  96. package/lib/module/use-stack-modifiers.js.map +1 -1
  97. package/lib/typescript/atoms/activity-indicator/activity-indicator.types.d.ts +3 -1
  98. package/lib/typescript/atoms/avatar/avatar.types.d.ts +7 -2
  99. package/lib/typescript/atoms/badge/badge.types.d.ts +1 -1
  100. package/lib/typescript/atoms/blur-view/blur-view.types.d.ts +6 -6
  101. package/lib/typescript/atoms/button/button-tokens.d.ts +1 -1
  102. package/lib/typescript/atoms/button/button.types.d.ts +9 -7
  103. package/lib/typescript/atoms/card/card.types.d.ts +3 -3
  104. package/lib/typescript/atoms/checkbox/checkbox.types.d.ts +2 -2
  105. package/lib/typescript/atoms/chip/chip.types.d.ts +1 -1
  106. package/lib/typescript/atoms/color-picker/color-picker.types.d.ts +4 -3
  107. package/lib/typescript/atoms/date-picker/date-picker.types.d.ts +4 -4
  108. package/lib/typescript/atoms/divider/divider.types.d.ts +2 -2
  109. package/lib/typescript/atoms/fab/fab.types.d.ts +10 -3
  110. package/lib/typescript/atoms/glass-view/glass-view.types.d.ts +5 -5
  111. package/lib/typescript/atoms/h-stack/h-stack.types.d.ts +8 -5
  112. package/lib/typescript/atoms/icon/icon.types.d.ts +4 -4
  113. package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +14 -3
  114. package/lib/typescript/atoms/image/image.d.ts +2 -2
  115. package/lib/typescript/atoms/island/island.types.d.ts +4 -4
  116. package/lib/typescript/atoms/list/list.types.d.ts +5 -5
  117. package/lib/typescript/atoms/picker/picker.types.d.ts +1 -1
  118. package/lib/typescript/atoms/pressable/pressable.types.d.ts +23 -7
  119. package/lib/typescript/atoms/progress/progress.types.d.ts +3 -1
  120. package/lib/typescript/atoms/rn-flat-list/rn-flat-list.types.d.ts +2 -2
  121. package/lib/typescript/atoms/rn-icon/rn-icon.types.d.ts +1 -1
  122. package/lib/typescript/atoms/rn-scroll-view/rn-scroll-view.types.d.ts +2 -2
  123. package/lib/typescript/atoms/rn-section-list/rn-section-list.types.d.ts +2 -2
  124. package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +2 -2
  125. package/lib/typescript/atoms/rn-view/rn-view.types.d.ts +3 -3
  126. package/lib/typescript/atoms/scroll-view/scroll-view.types.d.ts +3 -3
  127. package/lib/typescript/atoms/segmented-control/segmented-control.types.d.ts +3 -3
  128. package/lib/typescript/atoms/skeleton/skeleton.types.d.ts +2 -1
  129. package/lib/typescript/atoms/spacer/spacer.types.d.ts +1 -1
  130. package/lib/typescript/atoms/surface/surface.types.d.ts +3 -3
  131. package/lib/typescript/atoms/switch/switch.types.d.ts +1 -1
  132. package/lib/typescript/atoms/text/text.types.d.ts +3 -3
  133. package/lib/typescript/atoms/text-field/text-field.types.d.ts +4 -4
  134. package/lib/typescript/atoms/v-stack/v-stack.types.d.ts +4 -4
  135. package/lib/typescript/atoms/z-stack/z-stack.types.d.ts +4 -4
  136. package/lib/typescript/glyph/glyph-button.types.d.ts +1 -1
  137. package/lib/typescript/glyph/glyph.types.d.ts +3 -1
  138. package/lib/typescript/molecules/collapsible/collapsible.types.d.ts +1 -1
  139. package/lib/typescript/molecules/empty-state/empty-state.types.d.ts +1 -1
  140. package/lib/typescript/molecules/label/label.types.d.ts +1 -1
  141. package/lib/typescript/molecules/labeled-content/labeled-content.types.d.ts +6 -4
  142. package/lib/typescript/molecules/link/link.types.d.ts +2 -1
  143. package/lib/typescript/molecules/list-item/list-item.types.d.ts +6 -4
  144. package/lib/typescript/molecules/navigation-bar/navigation-bar.types.d.ts +8 -8
  145. package/lib/typescript/molecules/pager/pager.types.d.ts +2 -2
  146. package/lib/typescript/molecules/radio-group/radio-group.types.d.ts +2 -2
  147. package/lib/typescript/molecules/rn-list-item/rn-list-item.types.d.ts +1 -1
  148. package/lib/typescript/molecules/rn-tooltip/rn-tooltip.types.d.ts +7 -7
  149. package/lib/typescript/molecules/scroll-edge-container/scroll-edge-container.types.d.ts +3 -3
  150. package/lib/typescript/molecules/search-bar/search-bar.types.d.ts +2 -2
  151. package/lib/typescript/molecules/stepper/stepper.ios.d.ts +1 -1
  152. package/lib/typescript/molecules/stepper/stepper.types.d.ts +2 -2
  153. package/lib/typescript/molecules/tooltip/tooltip.types.d.ts +6 -6
  154. package/lib/typescript/motion.d.ts +8 -0
  155. package/lib/typescript/organisms/action-sheet/action-sheet.types.d.ts +3 -3
  156. package/lib/typescript/organisms/alert/alert.types.d.ts +3 -3
  157. package/lib/typescript/organisms/context-menu/context-menu.types.d.ts +9 -9
  158. package/lib/typescript/organisms/fab-menu/fab-menu.types.d.ts +10 -5
  159. package/lib/typescript/organisms/form/form.types.d.ts +1 -1
  160. package/lib/typescript/organisms/menu/menu.types.d.ts +2 -2
  161. package/lib/typescript/organisms/sheet/sheet.types.d.ts +12 -12
  162. package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +39 -6
  163. package/lib/typescript/organisms/swipe-actions/swipe-actions.types.d.ts +5 -5
  164. package/lib/typescript/templates/screen/screen.types.d.ts +2 -2
  165. package/lib/typescript/use-stack-modifiers.d.ts +6 -5
  166. package/package.json +4 -4
  167. package/src/atoms/activity-indicator/activity-indicator.android.tsx +7 -1
  168. package/src/atoms/activity-indicator/activity-indicator.ios.tsx +9 -1
  169. package/src/atoms/activity-indicator/activity-indicator.types.ts +3 -1
  170. package/src/atoms/avatar/avatar.tsx +5 -1
  171. package/src/atoms/avatar/avatar.types.ts +7 -2
  172. package/src/atoms/badge/badge.types.ts +1 -1
  173. package/src/atoms/blur-view/blur-view.types.ts +6 -6
  174. package/src/atoms/button/button-tokens.ts +2 -2
  175. package/src/atoms/button/button.android.tsx +5 -3
  176. package/src/atoms/button/button.ios.tsx +10 -2
  177. package/src/atoms/button/button.types.ts +9 -7
  178. package/src/atoms/card/card.android.tsx +6 -4
  179. package/src/atoms/card/card.ios.tsx +7 -3
  180. package/src/atoms/card/card.types.ts +9 -8
  181. package/src/atoms/checkbox/checkbox.android.tsx +1 -1
  182. package/src/atoms/checkbox/checkbox.types.ts +2 -2
  183. package/src/atoms/chip/chip.types.ts +1 -1
  184. package/src/atoms/color-picker/color-picker.android.tsx +26 -6
  185. package/src/atoms/color-picker/color-picker.types.ts +4 -3
  186. package/src/atoms/date-picker/date-picker.types.ts +4 -4
  187. package/src/atoms/divider/divider.types.ts +2 -2
  188. package/src/atoms/fab/fab.android.tsx +5 -3
  189. package/src/atoms/fab/fab.ios.tsx +8 -4
  190. package/src/atoms/fab/fab.types.ts +10 -3
  191. package/src/atoms/glass-view/glass-view.android.tsx +1 -1
  192. package/src/atoms/glass-view/glass-view.types.ts +5 -5
  193. package/src/atoms/h-stack/h-stack.ios.tsx +6 -4
  194. package/src/atoms/h-stack/h-stack.types.ts +18 -14
  195. package/src/atoms/icon/icon.types.ts +4 -4
  196. package/src/atoms/icon-button/icon-button.android.tsx +16 -3
  197. package/src/atoms/icon-button/icon-button.ios.tsx +24 -5
  198. package/src/atoms/icon-button/icon-button.types.ts +14 -3
  199. package/src/atoms/image/image.tsx +3 -3
  200. package/src/atoms/island/island.types.ts +4 -4
  201. package/src/atoms/list/list.types.ts +5 -5
  202. package/src/atoms/picker/picker.types.ts +1 -1
  203. package/src/atoms/pressable/pressable.tsx +50 -21
  204. package/src/atoms/pressable/pressable.types.ts +35 -17
  205. package/src/atoms/progress/progress.android.tsx +5 -3
  206. package/src/atoms/progress/progress.ios.tsx +5 -2
  207. package/src/atoms/progress/progress.types.ts +3 -1
  208. package/src/atoms/rn-flat-list/rn-flat-list.types.ts +2 -2
  209. package/src/atoms/rn-icon/rn-icon.types.ts +1 -1
  210. package/src/atoms/rn-scroll-view/rn-scroll-view.types.ts +2 -2
  211. package/src/atoms/rn-section-list/rn-section-list.types.ts +2 -2
  212. package/src/atoms/rn-text/rn-text.types.ts +2 -2
  213. package/src/atoms/rn-view/rn-view.types.ts +3 -3
  214. package/src/atoms/scroll-view/scroll-view.types.ts +3 -3
  215. package/src/atoms/segmented-control/segmented-control.android.tsx +1 -1
  216. package/src/atoms/segmented-control/segmented-control.ios.tsx +1 -1
  217. package/src/atoms/segmented-control/segmented-control.types.ts +3 -3
  218. package/src/atoms/skeleton/skeleton.tsx +18 -4
  219. package/src/atoms/skeleton/skeleton.types.ts +2 -1
  220. package/src/atoms/spacer/spacer.types.ts +1 -1
  221. package/src/atoms/surface/surface.android.tsx +6 -4
  222. package/src/atoms/surface/surface.ios.tsx +7 -3
  223. package/src/atoms/surface/surface.types.ts +11 -10
  224. package/src/atoms/switch/switch.android.tsx +1 -1
  225. package/src/atoms/switch/switch.types.ts +1 -1
  226. package/src/atoms/text/text.android.tsx +6 -4
  227. package/src/atoms/text/text.ios.tsx +6 -4
  228. package/src/atoms/text/text.types.ts +24 -23
  229. package/src/atoms/text-field/text-field.types.ts +4 -4
  230. package/src/atoms/v-stack/v-stack.ios.tsx +6 -4
  231. package/src/atoms/v-stack/v-stack.types.ts +18 -17
  232. package/src/atoms/z-stack/z-stack.ios.tsx +6 -4
  233. package/src/atoms/z-stack/z-stack.types.ts +16 -15
  234. package/src/glyph/glyph-button.types.ts +1 -1
  235. package/src/glyph/glyph.android.tsx +14 -2
  236. package/src/glyph/glyph.ios.tsx +13 -3
  237. package/src/glyph/glyph.types.ts +3 -1
  238. package/src/molecules/collapsible/collapsible.android.tsx +27 -7
  239. package/src/molecules/collapsible/collapsible.types.ts +1 -1
  240. package/src/molecules/empty-state/empty-state.types.ts +1 -1
  241. package/src/molecules/label/label.types.ts +1 -1
  242. package/src/molecules/labeled-content/labeled-content.tsx +12 -1
  243. package/src/molecules/labeled-content/labeled-content.types.ts +6 -4
  244. package/src/molecules/link/link.tsx +2 -1
  245. package/src/molecules/link/link.types.ts +2 -1
  246. package/src/molecules/list-item/list-item.tsx +20 -2
  247. package/src/molecules/list-item/list-item.types.ts +6 -4
  248. package/src/molecules/navigation-bar/navigation-bar.android.tsx +1 -1
  249. package/src/molecules/navigation-bar/navigation-bar.ios.tsx +1 -1
  250. package/src/molecules/navigation-bar/navigation-bar.types.ts +8 -8
  251. package/src/molecules/pager/pager.types.ts +2 -2
  252. package/src/molecules/radio-group/radio-group.types.ts +2 -2
  253. package/src/molecules/rn-list-item/rn-list-item.tsx +18 -3
  254. package/src/molecules/rn-list-item/rn-list-item.types.ts +1 -1
  255. package/src/molecules/rn-tooltip/rn-tooltip.types.ts +7 -7
  256. package/src/molecules/scroll-edge-container/scroll-edge-container.android.tsx +3 -2
  257. package/src/molecules/scroll-edge-container/scroll-edge-container.types.ts +3 -3
  258. package/src/molecules/search-bar/search-bar.types.ts +2 -2
  259. package/src/molecules/stepper/stepper.ios.tsx +8 -2
  260. package/src/molecules/stepper/stepper.types.ts +2 -2
  261. package/src/molecules/tooltip/tooltip.types.ts +6 -6
  262. package/src/motion.ts +13 -0
  263. package/src/organisms/action-sheet/action-sheet.types.ts +3 -3
  264. package/src/organisms/alert/alert.types.ts +3 -3
  265. package/src/organisms/context-menu/context-menu.types.ts +9 -9
  266. package/src/organisms/fab-menu/fab-menu.android.tsx +9 -5
  267. package/src/organisms/fab-menu/fab-menu.ios.tsx +6 -2
  268. package/src/organisms/fab-menu/fab-menu.types.ts +10 -5
  269. package/src/organisms/form/form.types.ts +1 -1
  270. package/src/organisms/menu/menu.types.ts +2 -2
  271. package/src/organisms/sheet/sheet.types.ts +12 -12
  272. package/src/organisms/snackbar/snackbar.tsx +143 -19
  273. package/src/organisms/snackbar/snackbar.types.ts +42 -6
  274. package/src/organisms/swipe-actions/swipe-actions.android.tsx +1 -1
  275. package/src/organisms/swipe-actions/swipe-actions.types.ts +5 -5
  276. package/src/templates/screen/screen.types.ts +2 -2
  277. package/src/use-stack-modifiers.ts +20 -7
  278. package/lib/module/platform-support.js +0 -19
  279. package/lib/module/platform-support.js.map +0 -1
  280. package/lib/typescript/platform-support.d.ts +0 -8
  281. package/src/platform-support.ts +0 -20
@@ -1,23 +1,23 @@
1
1
  import type {ReactNode} from 'react'
2
2
 
3
3
  /**
4
- * A scrolling list — SwiftUI `List`, Compose `LazyColumn`. It has no height of its own, so it needs one from outside:
4
+ * A scrolling list: SwiftUI `List`, Compose `LazyColumn`. It has no height of its own, so it needs one from outside:
5
5
  * a `Screen`, or an `Island` with `matchContents={false}` and a height. In an island that sizes itself to its content
6
6
  * Compose measures it against an infinite height and throws ("Vertically scrollable component was measured with an
7
7
  * infinity maximum height"); SwiftUI shows nothing. For a handful of rows that should not scroll, use a `VStack`.
8
8
  *
9
- * Rows are separated for you — SwiftUI's own separators, a `Divider` between rows on Android — so a `Divider` among
9
+ * Rows are separated for you (SwiftUI's own separators, a `Divider` between rows on Android), so a `Divider` among
10
10
  * the children is one too many: on iOS it becomes an empty row of its own.
11
11
  */
12
12
  export type ListProps = {
13
- /** The rows — native views, one row each. */
13
+ /** The rows: native views, one row each. */
14
14
  children: ReactNode
15
15
  /**
16
- * Enables pull-to-refresh. Async on purpose — see `ScrollViewProps.onRefresh`: the spinner runs
16
+ * Enables pull-to-refresh. Async on purpose (see `ScrollViewProps.onRefresh`): the spinner runs
17
17
  * until the promise settles, so neither platform needs a `refreshing` flag kept in sync by hand.
18
18
  */
19
19
  onRefresh?: () => Promise<void>
20
- /** Test identifier, forwarded to the native view on iOS — Android ignores it. */
20
+ /** Test identifier, forwarded to the native view on iOS. Android ignores it. */
21
21
  testID?: string
22
22
  }
23
23
 
@@ -9,7 +9,7 @@ export type PickerOption = {
9
9
  }
10
10
 
11
11
  /**
12
- * A single choice from a list — a SwiftUI wheel picker on iOS, Material's exposed dropdown on Android. It brings its
12
+ * A single choice from a list: a SwiftUI wheel picker on iOS, Material's exposed dropdown on Android. It brings its
13
13
  * own host, so it renders outside `Screen` and `Island` as well.
14
14
  */
15
15
  export type PickerProps = {
@@ -1,14 +1,23 @@
1
- import {useSplitSx} from '@hzblj/kara-sx'
2
- import {useAppTheme, withOpacity} from '@hzblj/kara-theme'
1
+ import {accessibilityProps, splitAccessibility, useSplitSx} from '@hzblj/kara-sx'
2
+ import {opacity, useAppTheme, useReducedMotion, withOpacity} from '@hzblj/kara-theme'
3
3
  import {useCallback, useMemo} from 'react'
4
- import {Platform, Pressable as RNPressable, View, type ViewStyle} from 'react-native'
5
- import Animated, {useAnimatedStyle, useSharedValue, withSpring} from 'react-native-reanimated'
4
+ import {Platform, Pressable as RNPressable, type ViewStyle} from 'react-native'
5
+ import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'
6
6
 
7
7
  import {haptics} from '../../haptics'
8
+ import {timing} from '../../motion'
8
9
  import {splitForRipple} from '../../ripple-clip'
9
10
  import type {PressableComponent, PressableProps} from './pressable.types'
10
11
 
11
- const SPRING = {damping: 20, stiffness: 400}
12
+ /** The scale eases down and settles back a little slower, with no bounce: a press is not a toy. */
13
+ const SCALE_IN = timing('quick')
14
+ const SCALE_OUT = timing('fast')
15
+
16
+ /** A dim comes in at once and leaves a little slower, as UIKit's own highlight does. */
17
+ const DIM_IN = timing('micro', 'out')
18
+ const DIM_OUT = timing('quick', 'out')
19
+
20
+ const PLATFORM_EFFECT = Platform.OS === 'ios' ? 'scale' : 'none'
12
21
 
13
22
  const AnimatedPressable = Animated.createAnimatedComponent(RNPressable)
14
23
 
@@ -17,24 +26,43 @@ export const Pressable: PressableComponent = ({
17
26
  disabled = false,
18
27
  haptic = false,
19
28
  onPress,
29
+ pressEffect = PLATFORM_EFFECT,
30
+ pressedOpacity = 'o60',
31
+ pressedScale = 0.96,
32
+ ripple: withRipple = true,
20
33
  testID,
21
34
  ...props
22
35
  }: PressableProps) => {
23
- const [resolved, rest] = useSplitSx(props)
24
- const scale = useSharedValue(1)
36
+ const [accessibility, ownProps] = splitAccessibility(props)
37
+ const [resolved, rest] = useSplitSx(ownProps)
38
+ const reduced = useReducedMotion()
39
+ // With motion reduced the press still shows, and nothing moves: a scale dims instead.
40
+ const effect = reduced && pressEffect === 'scale' ? 'opacity' : pressEffect
41
+ // 0 at rest, 1 held: one value drives whichever effect is picked, so a change of effect needs no second animation.
42
+ const pressed = useSharedValue(0)
25
43
  const feedback = haptic === true ? 'light' : haptic || undefined
44
+ const a11y = accessibilityProps(accessibility, 'button')
45
+ const dimmed = opacity[pressedOpacity]
26
46
 
27
47
  const theme = useAppTheme()
28
48
  const isAndroid = Platform.OS === 'android'
29
49
 
30
- const rStyle = useAnimatedStyle(() => ({transform: [{scale: scale.value}]}), [])
50
+ const rStyle = useAnimatedStyle(() => {
51
+ if (effect === 'scale') {
52
+ return {transform: [{scale: 1 - (1 - pressedScale) * pressed.value}]}
53
+ }
31
54
 
32
- // Android's press is Material's: a ripple over the content — the pressed state layer, `onSurface` at 10 % — clipped to
33
- // the shape by an outer view (`splitForRipple`), and nothing moves. iOS springs down.
55
+ return effect === 'opacity' ? {opacity: 1 - (1 - dimmed) * pressed.value} : {}
56
+ }, [dimmed, effect, pressedScale])
57
+
58
+ // Android's press is Material's: a ripple over the content (the pressed state layer, `onSurface` at 10 %) clipped to
59
+ // the shape by an outer view (`splitForRipple`). The effect, when there is one, moves that outer view, ripple and all.
34
60
  const ripple = useMemo(
35
61
  () =>
36
- isAndroid ? {color: withOpacity(String(theme.color('content.primary')), 'o10'), foreground: true} : undefined,
37
- [isAndroid, theme]
62
+ isAndroid && withRipple
63
+ ? {color: withOpacity(String(theme.color('content.primary')), 'o10'), foreground: true}
64
+ : undefined,
65
+ [isAndroid, theme, withRipple]
38
66
  )
39
67
  const androidStyle = useMemo(
40
68
  () => (isAndroid ? splitForRipple(resolved as ViewStyle) : undefined),
@@ -43,20 +71,18 @@ export const Pressable: PressableComponent = ({
43
71
 
44
72
  // The finger is down: the Taptic Engine has until the release to wake, so the haptic lands with the press.
45
73
  const onPressIn = useCallback(() => {
46
- if (!isAndroid) {
47
- scale.value = withSpring(0.96, SPRING)
74
+ if (effect !== 'none') {
75
+ pressed.value = withTiming(1, effect === 'scale' ? SCALE_IN : DIM_IN)
48
76
  }
49
77
 
50
78
  if (feedback) {
51
79
  haptics.prepare(feedback)
52
80
  }
53
- }, [feedback, isAndroid, scale])
81
+ }, [effect, feedback, pressed])
54
82
 
55
83
  const onPressOut = useCallback(() => {
56
- if (!isAndroid) {
57
- scale.value = withSpring(1, SPRING)
58
- }
59
- }, [isAndroid, scale])
84
+ pressed.value = withTiming(0, effect === 'scale' ? SCALE_OUT : DIM_OUT)
85
+ }, [effect, pressed])
60
86
 
61
87
  const onPressWithHaptic = useCallback(() => {
62
88
  if (feedback) {
@@ -68,25 +94,28 @@ export const Pressable: PressableComponent = ({
68
94
 
69
95
  if (androidStyle) {
70
96
  return (
71
- <View style={androidStyle.outer}>
97
+ <Animated.View style={[androidStyle.outer, rStyle]}>
72
98
  <RNPressable
99
+ {...a11y}
73
100
  android_ripple={ripple}
74
101
  disabled={disabled}
75
102
  hitSlop={8}
76
103
  onPress={onPressWithHaptic}
77
104
  onPressIn={onPressIn}
105
+ onPressOut={onPressOut}
78
106
  style={androidStyle.inner}
79
107
  testID={testID}
80
108
  {...rest}
81
109
  >
82
110
  {children}
83
111
  </RNPressable>
84
- </View>
112
+ </Animated.View>
85
113
  )
86
114
  }
87
115
 
88
116
  return (
89
117
  <AnimatedPressable
118
+ {...a11y}
90
119
  disabled={disabled}
91
120
  hitSlop={8}
92
121
  onPress={onPressWithHaptic}
@@ -1,25 +1,43 @@
1
- import type {ResponsiveViewSx} from '@hzblj/kara-sx'
1
+ import type {AccessibilityProps, ResponsiveViewSx} from '@hzblj/kara-sx'
2
+ import type {OpacitySize} from '@hzblj/kara-theme'
2
3
  import type {ReactNode} from 'react'
3
4
 
4
5
  import type {Haptic} from '../../haptics'
5
6
 
7
+ /** How the content answers a press: it eases down, it dims, or it stays still. */
8
+ export type PressEffect = 'none' | 'opacity' | 'scale'
9
+
6
10
  /**
7
- * React Native's `Pressable`, styled through `sx` like a `Box`, taking touches 8 points past its edges. The press is
8
- * each platform's own: iOS springs it down to 96%; Android draws Material's ripple over the content, clipped to the
9
- * pressable's shape — give it the `borderRadius` of what it wraps (`full` around a `GlassView`). The React Native
10
- * tier — it needs no host.
11
+ * React Native's `Pressable`, styled through `sx` like a `Box`, taking touches 8 points past its edges. Each platform
12
+ * presses its own way by default: iOS eases the content down to 96%, Android draws Material's ripple over it,
13
+ * clipped to the pressable's shape. `pressEffect` picks the same answer on both, and Android keeps its ripple on top
14
+ * unless `ripple` is off. Give it the `borderRadius` of what it wraps (`full` around a `GlassView`). Screen readers read
15
+ * it as a button unless `accessibilityRole` names another role. The React Native tier: it needs no host.
11
16
  */
12
- export type PressableProps = ResponsiveViewSx & {
13
- /** The pressable content. */
14
- children?: ReactNode
15
- /** Ignores presses — no `onPress`, no haptic, no spring or ripple. */
16
- disabled?: boolean
17
- /** Played on press. `true` is a `light` impact; a name picks the haptic, e.g. `selection` for a choice. */
18
- haptic?: boolean | Haptic
19
- /** Called when the press is released, after the `haptic` plays. */
20
- onPress?: () => void
21
- /** Test identifier, forwarded to the view. */
22
- testID?: string
23
- }
17
+ export type PressableProps = ResponsiveViewSx &
18
+ AccessibilityProps & {
19
+ /** The pressable content. */
20
+ children?: ReactNode
21
+ /** Ignores presses: no `onPress`, no haptic, no press effect or ripple. */
22
+ disabled?: boolean
23
+ /** Played on press. `true` is a `light` impact; a name picks the haptic, e.g. `selection` for a choice. */
24
+ haptic?: boolean | Haptic
25
+ /** Called when the press is released, after the `haptic` plays. */
26
+ onPress?: () => void
27
+ /**
28
+ * How the content answers while it is held. `scale` eases it down to `pressedScale`, `opacity` dims it to
29
+ * `pressedOpacity`, `none` leaves it still. `scale` on iOS and `none` on Android by default, where the ripple is the
30
+ * press. With motion reduced, `scale` dims instead: the press still shows, and nothing moves.
31
+ */
32
+ pressEffect?: PressEffect
33
+ /** What `opacity` dims to, an opacity token. `o60` by default. */
34
+ pressedOpacity?: OpacitySize
35
+ /** What `scale` eases down to. `0.96` by default: lower reads as a squash, not a press. */
36
+ pressedScale?: number
37
+ /** Android's ripple: Material's pressed state layer over the content, clipped to the shape. On by default. iOS has none. */
38
+ ripple?: boolean
39
+ /** Test identifier, forwarded to the view. */
40
+ testID?: string
41
+ }
24
42
 
25
43
  export type PressableComponent = (props: PressableProps) => ReactNode
@@ -1,17 +1,19 @@
1
1
  import {CircularProgressIndicator, LinearProgressIndicator} from '@expo/ui/jetpack-compose'
2
+ import {useAccessibilityModifiers} from '@hzblj/kara-sx'
2
3
  import {useAppTheme} from '@hzblj/kara-theme'
3
4
 
4
5
  import {useHostGuard} from '../../host-context'
5
6
  import type {ProgressComponent, ProgressProps} from './progress.types'
6
7
 
7
- export const Progress: ProgressComponent = ({circular = false, value}: ProgressProps) => {
8
+ export const Progress: ProgressComponent = ({accessibilityLabel, circular = false, value}: ProgressProps) => {
8
9
  useHostGuard('Progress')
9
10
  const theme = useAppTheme()
10
11
  const color = theme.color('fill.accent.primary')
12
+ const modifiers = useAccessibilityModifiers({accessibilityLabel})
11
13
 
12
14
  if (circular) {
13
- return <CircularProgressIndicator color={color} progress={value} />
15
+ return <CircularProgressIndicator color={color} modifiers={modifiers} progress={value} />
14
16
  }
15
17
 
16
- return <LinearProgressIndicator color={color} progress={value} />
18
+ return <LinearProgressIndicator color={color} modifiers={modifiers} progress={value} />
17
19
  }
@@ -1,22 +1,25 @@
1
1
  import {Gauge, ProgressView} from '@expo/ui/swift-ui'
2
2
  import {gaugeStyle, progressViewStyle, tint} from '@expo/ui/swift-ui/modifiers'
3
+ import {useAccessibilityModifiers} from '@hzblj/kara-sx'
3
4
  import {useAppTheme} from '@hzblj/kara-theme'
4
5
  import {useMemo} from 'react'
5
6
 
6
7
  import {useHostGuard} from '../../host-context'
7
8
  import type {ProgressComponent, ProgressProps} from './progress.types'
8
9
 
9
- export const Progress: ProgressComponent = ({circular = false, value}: ProgressProps) => {
10
+ export const Progress: ProgressComponent = ({accessibilityLabel, circular = false, value}: ProgressProps) => {
10
11
  useHostGuard('Progress')
11
12
  const theme = useAppTheme()
13
+ const accessibilityModifiers = useAccessibilityModifiers({accessibilityLabel})
12
14
 
13
15
  const ring = circular && value !== undefined
14
16
  const modifiers = useMemo(
15
17
  () => [
16
18
  ring ? gaugeStyle('circularCapacity') : progressViewStyle(circular ? 'circular' : 'linear'),
17
19
  tint(theme.color('fill.accent.primary')),
20
+ ...accessibilityModifiers,
18
21
  ],
19
- [circular, ring, theme]
22
+ [accessibilityModifiers, circular, ring, theme]
20
23
  )
21
24
 
22
25
  // SwiftUI's circular `ProgressView` ignores its value on iOS and spins; a ring that fills is a capacity `Gauge`.
@@ -1,10 +1,12 @@
1
1
  import type {ReactNode} from 'react'
2
2
 
3
3
  /**
4
- * A progress bar or ring, tinted `fill.accent.primary` — SwiftUI `ProgressView` (a capacity `Gauge` for a ring with a
4
+ * A progress bar or ring, tinted `fill.accent.primary`: SwiftUI `ProgressView` (a capacity `Gauge` for a ring with a
5
5
  * value, which `ProgressView` cannot draw on iOS) / Compose's `LinearProgressIndicator` and `CircularProgressIndicator`.
6
6
  */
7
7
  export type ProgressProps = {
8
+ /** What is in progress, for VoiceOver and TalkBack: "Uploading". They read how far along it is themselves. */
9
+ accessibilityLabel?: string
8
10
  /** A circular indicator instead of a bar. Off by default. */
9
11
  circular?: boolean
10
12
  /** Between `0` and `1`. Leave it unset for an indeterminate indicator. */
@@ -3,11 +3,11 @@ import type {FlatListProps as RNFlatListProps} from 'react-native'
3
3
 
4
4
  /**
5
5
  * Top-level `sx` styles the viewport; `content` styles the scrolling content container, so padding and
6
- * gap belong there — the same split as `RnScrollView`. `removeClippedSubviews` is off unless you turn it on:
6
+ * gap belong there (the same split as `RnScrollView`). `removeClippedSubviews` is off unless you turn it on:
7
7
  * Android's default would detach a row's host before it has measured itself, and the row would stay empty.
8
8
  */
9
9
  export type RnFlatListProps<TItem> = ResponsiveViewSx &
10
10
  Omit<RNFlatListProps<TItem>, 'contentContainerStyle' | 'style'> & {
11
- /** Styles the scrolling content container — padding and gap belong here. */
11
+ /** Styles the scrolling content container; padding and gap belong here. */
12
12
  content?: ResponsiveViewSx
13
13
  }
@@ -2,7 +2,7 @@ import type {ReactNode} from 'react'
2
2
 
3
3
  import type {IconProps} from '../icon'
4
4
 
5
- /** The same contract as `Icon`, for the React Native tier — anywhere, no `Screen` or `Island` needed. */
5
+ /** The same contract as `Icon`, for the React Native tier: anywhere, no `Screen` or `Island` needed. */
6
6
  export type RnIconProps = IconProps
7
7
 
8
8
  export type RnIconComponent = (props: RnIconProps) => ReactNode
@@ -4,7 +4,7 @@ import type {ScrollViewProps as RNScrollViewProps} from 'react-native'
4
4
  import type {ScrollViewAxis} from '../scroll-view'
5
5
 
6
6
  /**
7
- * React Native tier scroll container — the sibling of `RnView`/`RnText`. Use it when the
7
+ * React Native tier scroll container, the sibling of `RnView`/`RnText`. Use it when the
8
8
  * subtree is React Native and native atoms are embedded through `Island`. For a screen that
9
9
  * is native all the way down, reach for the `ScrollView` atom instead.
10
10
  *
@@ -17,7 +17,7 @@ export type RnScrollViewProps = ResponsiveViewSx &
17
17
  axis?: ScrollViewAxis
18
18
  /** The scrolling content. */
19
19
  children?: ReactNode
20
- /** Styles the scrolling content container — padding and gap belong here. */
20
+ /** Styles the scrolling content container; padding and gap belong here. */
21
21
  content?: ResponsiveViewSx
22
22
  /** Shows the scroll indicators. On by default. */
23
23
  showsIndicators?: boolean
@@ -3,11 +3,11 @@ import type {DefaultSectionT, SectionListProps as RNSectionListProps} from 'reac
3
3
 
4
4
  /**
5
5
  * Top-level `sx` styles the viewport; `content` styles the scrolling content container, so padding and
6
- * gap belong there — the same split as `RnScrollView` and `RnFlatList`. `removeClippedSubviews` is off unless
6
+ * gap belong there (the same split as `RnScrollView` and `RnFlatList`). `removeClippedSubviews` is off unless
7
7
  * you turn it on, as in `RnFlatList`: Android's default would leave rows with hosts in them empty.
8
8
  */
9
9
  export type RnSectionListProps<TItem, TSection = DefaultSectionT> = ResponsiveViewSx &
10
10
  Omit<RNSectionListProps<TItem, TSection>, 'contentContainerStyle' | 'style'> & {
11
- /** Styles the scrolling content container — padding and gap belong here. */
11
+ /** Styles the scrolling content container; padding and gap belong here. */
12
12
  content?: ResponsiveViewSx
13
13
  }
@@ -4,8 +4,8 @@ import type {ReactNode} from 'react'
4
4
  import type {TextProps as RNTextProps} from 'react-native'
5
5
 
6
6
  /**
7
- * React Native's `Text` in a theme typography variant — the React Native tier, for text outside a native host. It takes
8
- * `Box`'s `sx` surface for its own box — margins, flex, padding, a background — all but `align`, which here aligns the
7
+ * React Native's `Text` in a theme typography variant: the React Native tier, for text outside a native host. It takes
8
+ * `Box`'s `sx` surface for its own box (margins, flex, padding, a background), all but `align`, which here aligns the
9
9
  * lines.
10
10
  */
11
11
  export type RnTextProps = Omit<RNTextProps, 'style'> &
@@ -1,12 +1,12 @@
1
1
  import type {ReactElement, ReactNode} from 'react'
2
2
 
3
- /** Carries a React Native view into a SwiftUI or Compose tree — `@expo/ui`'s `RNHostView` on both platforms. */
3
+ /** Carries a React Native view into a SwiftUI or Compose tree: `@expo/ui`'s `RNHostView` on both platforms. */
4
4
  export type RnViewProps = {
5
5
  /**
6
6
  * A single React Native *view*. It may not contain React Native text: a bare string becomes a
7
7
  * `RawText` node, which has no component view, so mounting it across the SwiftUI host boundary
8
8
  * fails the `RCTComponentViewFactory` assertion and takes the whole app down. `@expo/ui` states
9
- * the rule itself in `ListItem.ios.tsx` — "SwiftUI hosts can't render bare strings" — and wraps
9
+ * the rule itself in `ListItem.ios.tsx` ("SwiftUI hosts can't render bare strings") and wraps
10
10
  * every raw string in a SwiftUI `Text` before it reaches a host. Do the same: use the `Text` atom
11
11
  * inside the host, and keep `RnText` outside it.
12
12
  */
@@ -16,7 +16,7 @@ export type RnViewProps = {
16
16
  * intrinsic size and would otherwise stretch to fill the SwiftUI parent.
17
17
  */
18
18
  matchContents?: boolean
19
- /** Not forwarded on either platform — give the child its own `testID`. */
19
+ /** Not forwarded on either platform: give the child its own `testID`. */
20
20
  testID?: string
21
21
  }
22
22
 
@@ -5,7 +5,7 @@ import type {ReactNode} from 'react'
5
5
  export type ScrollViewAxis = 'horizontal' | 'vertical'
6
6
 
7
7
  /**
8
- * The `sx` props style the scrolling *content* container, not the viewport — `p` behaves
8
+ * The `sx` props style the scrolling *content* container, not the viewport: `p` behaves
9
9
  * like React Native's `contentContainerStyle` padding and scrolls with the content.
10
10
  */
11
11
  export type ScrollViewProps = ResponsiveSx & {
@@ -15,13 +15,13 @@ export type ScrollViewProps = ResponsiveSx & {
15
15
  children?: ReactNode
16
16
  /**
17
17
  * Enables pull-to-refresh. Async on purpose: the spinner runs until the promise settles, which is
18
- * what SwiftUI's `refreshable` expects and what Compose's controlled indicator is driven from — so
18
+ * what SwiftUI's `refreshable` expects and what Compose's controlled indicator is driven from, so
19
19
  * neither platform needs a separate `refreshing` flag to be kept in sync by the caller.
20
20
  *
21
21
  * Only a vertical scroll view can be pulled; on a horizontal one both platforms ignore it.
22
22
  */
23
23
  onRefresh?: () => Promise<void>
24
- /** iOS only — Compose exposes no indicator toggle, so Android always shows the native one. */
24
+ /** iOS only: Compose exposes no indicator toggle, so Android always shows the native one. */
25
25
  showsIndicators?: boolean
26
26
  /** The space between children. */
27
27
  spacing?: SpaceSize
@@ -29,7 +29,7 @@ export const SegmentedControl: SegmentedControlComponent = ({
29
29
  () => ({
30
30
  activeBorderColor: theme.color('fill.accent.primary'),
31
31
  activeContainerColor: theme.color('fill.accent.primary'),
32
- activeContentColor: theme.color('content.on.bg'),
32
+ activeContentColor: theme.color('content.on.accent'),
33
33
  disabledActiveBorderColor: theme.color('fill.accent.disabled'),
34
34
  disabledActiveContainerColor: theme.color('fill.accent.disabled'),
35
35
  disabledActiveContentColor: theme.color('btn.content.disabled'),
@@ -33,7 +33,7 @@ export const SegmentedControl: SegmentedControlComponent = ({
33
33
  return (
34
34
  <NativeSegmentedControl
35
35
  activePillColor={theme.color('fill.accent.primary')}
36
- activeTextColor={theme.color('content.on.bg')}
36
+ activeTextColor={theme.color('content.on.accent')}
37
37
  disabled={disabled}
38
38
  fontFamily={face.fontFamily}
39
39
  fontSize={face.fontSize}
@@ -1,7 +1,7 @@
1
1
  import type {ReactNode} from 'react'
2
2
 
3
3
  /**
4
- * A row of mutually exclusive segments, the selected one in the accent — kara's own `UISegmentedControl` on iOS,
4
+ * A row of mutually exclusive segments, the selected one in the accent: kara's own `UISegmentedControl` on iOS,
5
5
  * Material's single-choice segmented buttons on Android. It needs no `Screen` or `Island`: iOS is a plain view, Android
6
6
  * brings its own host. It takes the width it is given; the height is the platform's.
7
7
  */
@@ -9,13 +9,13 @@ export type SegmentedControlProps = {
9
9
  /** Stops the user changing the segment, and dims the control. */
10
10
  disabled?: boolean
11
11
  /**
12
- * Called with the index and label of the segment the user picked — never for a tap on the one already selected.
12
+ * Called with the index and label of the segment the user picked, never for a tap on the one already selected.
13
13
  * Android plays `selection`; iOS's control plays its own feedback.
14
14
  */
15
15
  onChange?: (index: number, value: string) => void
16
16
  /** The selected segment. Controlled: pass back what `onChange` reports. */
17
17
  selectedIndex: number
18
- /** Test identifier, on the whole control — reach a segment by its label. */
18
+ /** Test identifier, on the whole control. Reach a segment by its label. */
19
19
  testID?: string
20
20
  /** The segments' labels, in order. */
21
21
  values: string[]
@@ -1,21 +1,35 @@
1
- import {duration, opacity} from '@hzblj/kara-theme'
1
+ import {duration, opacity, useReducedMotion} from '@hzblj/kara-theme'
2
2
  import {useEffect} from 'react'
3
- import {useAnimatedStyle, useSharedValue, withRepeat, withTiming} from 'react-native-reanimated'
3
+ import {cancelAnimation, useAnimatedStyle, useSharedValue, withRepeat, withTiming} from 'react-native-reanimated'
4
4
 
5
+ import {timing} from '../../motion'
5
6
  import {ABox} from '../box'
6
7
  import type {SkeletonComponent, SkeletonProps} from './skeleton.types'
7
8
 
9
+ /** A breath, not a transition, so it sits off the scale: `slow` twice each way, at constant speed. */
10
+ const PULSE = {...timing('slow', 'linear'), duration: duration.slow * 2}
11
+
8
12
  export const Skeleton: SkeletonComponent = ({borderRadius = 'small', h, testID, w}: SkeletonProps) => {
13
+ const reduced = useReducedMotion()
9
14
  const pulse = useSharedValue<number>(opacity.o40)
10
15
 
16
+ // With motion reduced it holds still, halfway between the two ends of the pulse.
11
17
  useEffect(() => {
12
- pulse.value = withRepeat(withTiming(opacity.o100, {duration: duration.d800}), -1, true)
13
- }, [pulse])
18
+ if (reduced) {
19
+ cancelAnimation(pulse)
20
+ pulse.value = opacity.o70
21
+ return
22
+ }
23
+
24
+ pulse.value = opacity.o40
25
+ pulse.value = withRepeat(withTiming(opacity.o100, PULSE), -1, true)
26
+ }, [pulse, reduced])
14
27
 
15
28
  const rStyle = useAnimatedStyle(() => ({opacity: pulse.value}), [])
16
29
 
17
30
  return (
18
31
  <ABox
32
+ aria-hidden
19
33
  bg="fill.quaternary"
20
34
  borderRadius={borderRadius}
21
35
  h={h}
@@ -3,7 +3,8 @@ import type {RadiiSize} from '@hzblj/kara-theme'
3
3
  import type {ReactNode} from 'react'
4
4
 
5
5
  /**
6
- * A loading placeholder that pulses in `fill.quaternary` — a React Native view, so it needs no host. Sized with the
6
+ * A loading placeholder that pulses in `fill.quaternary`, and holds still with motion reduced. Hidden from screen
7
+ * readers: what is loading is for the screen to say. It is a React Native view, so it needs no host. Sized with the
7
8
  * `sx` names: `w`, `h`, `borderRadius`.
8
9
  */
9
10
  export type SkeletonProps = {
@@ -1,7 +1,7 @@
1
1
  import type {SpaceSize} from '@hzblj/kara-theme'
2
2
  import type {ReactNode} from 'react'
3
3
 
4
- /** Takes up the free space in a stack — SwiftUI `Spacer` / Compose `Spacer` with `weight(1)`. */
4
+ /** Takes up the free space in a stack: SwiftUI `Spacer` / Compose `Spacer` with `weight(1)`. */
5
5
  export type SpacerProps = {
6
6
  /** The least room it takes. iOS holds it along the stack's axis; Android as a minimum height only. */
7
7
  min?: SpaceSize
@@ -1,6 +1,6 @@
1
1
  import {Column, Surface as ComposeSurface, Shape} from '@expo/ui/jetpack-compose'
2
2
  import {testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
3
- import {useSxModifiers} from '@hzblj/kara-sx'
3
+ import {splitAccessibility, useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
4
  import {radii, useAppTheme} from '@hzblj/kara-theme'
5
5
  import {useMemo} from 'react'
6
6
  import {useHostGuard} from '../../host-context'
@@ -24,15 +24,17 @@ export const Surface: SurfaceComponent = ({
24
24
  px,
25
25
  py,
26
26
  testID,
27
- ...sx
27
+ ...props
28
28
  }: SurfaceProps) => {
29
29
  useHostGuard('Surface')
30
+ const [accessibility, sx] = splitAccessibility(props)
30
31
 
31
32
  const theme = useAppTheme()
32
33
  const sxModifiers = useSxModifiers({...sx, bg: undefined})
34
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility)
33
35
  const modifiers = useMemo(
34
- () => (testID ? [...sxModifiers, testIdModifier(testID)] : sxModifiers),
35
- [sxModifiers, testID]
36
+ () => [...sxModifiers, ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers],
37
+ [accessibilityModifiers, sxModifiers, testID]
36
38
  )
37
39
  const contentModifiers = useSxModifiers({p, pb, pl, pr, pt, px, py, wide: sx.wide})
38
40
 
@@ -1,13 +1,17 @@
1
1
  import {GlassEffectContainer, VStack} from '@expo/ui/swift-ui'
2
- import {useSxModifiers} from '@hzblj/kara-sx'
2
+ import {splitAccessibility, useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
3
+ import {useMemo} from 'react'
3
4
  import {useHostGuard} from '../../host-context'
4
5
  import type {SurfaceComponent, SurfaceProps} from './surface.types'
5
6
 
6
- export const Surface: SurfaceComponent = ({children, glass = false, testID, ...sx}: SurfaceProps) => {
7
+ export const Surface: SurfaceComponent = ({children, glass = false, testID, ...props}: SurfaceProps) => {
7
8
  useHostGuard('Surface')
9
+ const [accessibility, sx] = splitAccessibility(props)
8
10
 
9
11
  // `surface.primary` unless told otherwise, as Material's `Surface` is on Android — without it a plain one drew nothing.
10
- const modifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', ...sx})
12
+ const sxModifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', ...sx})
13
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility)
14
+ const modifiers = useMemo(() => [...sxModifiers, ...accessibilityModifiers], [accessibilityModifiers, sxModifiers])
11
15
  const content = (
12
16
  <VStack alignment="leading" modifiers={modifiers} testID={testID}>
13
17
  {children}
@@ -1,17 +1,18 @@
1
- import type {ResponsiveSx} from '@hzblj/kara-sx'
1
+ import type {AccessibilityProps, ResponsiveSx} from '@hzblj/kara-sx'
2
2
  import type {ReactNode} from 'react'
3
3
 
4
4
  /**
5
- * A container on `surface.primary`, padded `s16` and rounded `large` unless told otherwise — a SwiftUI `VStack` on
5
+ * A container on `surface.primary`, padded `s16` and rounded `large` unless told otherwise: a SwiftUI `VStack` on
6
6
  * iOS, Material's `Surface` around a `Column` on Android.
7
7
  */
8
- export type SurfaceProps = ResponsiveSx & {
9
- /** Stacked vertically, from the leading edge. */
10
- children?: ReactNode
11
- /** iOS wraps the content in a `GlassEffectContainer`; Android raises it to a tonal elevation of `3`. */
12
- glass?: boolean
13
- /** Test identifier, forwarded to the native view. */
14
- testID?: string
15
- }
8
+ export type SurfaceProps = ResponsiveSx &
9
+ AccessibilityProps & {
10
+ /** Stacked vertically, from the leading edge. */
11
+ children?: ReactNode
12
+ /** iOS wraps the content in a `GlassEffectContainer`; Android raises it to a tonal elevation of `3`. */
13
+ glass?: boolean
14
+ /** Test identifier, forwarded to the native view. */
15
+ testID?: string
16
+ }
16
17
 
17
18
  export type SurfaceComponent = (props: SurfaceProps) => ReactNode
@@ -34,7 +34,7 @@ export const Switch: SwitchComponent = ({disabled = false, label, onValueChange,
34
34
  <ComposeSwitch
35
35
  colors={{
36
36
  checkedBorderColor: theme.color('fill.accent.primary'),
37
- checkedThumbColor: theme.color('content.on.bg'),
37
+ checkedThumbColor: theme.color('content.on.accent'),
38
38
  checkedTrackColor: theme.color('fill.accent.primary'),
39
39
  }}
40
40
  enabled={!disabled}