@hzblj/kara-components 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/lib/module/atoms/activity-indicator/activity-indicator.android.js +6 -1
  3. package/lib/module/atoms/activity-indicator/activity-indicator.android.js.map +1 -1
  4. package/lib/module/atoms/activity-indicator/activity-indicator.ios.js +2 -0
  5. package/lib/module/atoms/activity-indicator/activity-indicator.ios.js.map +1 -1
  6. package/lib/module/atoms/avatar/avatar.js +5 -0
  7. package/lib/module/atoms/avatar/avatar.js.map +1 -1
  8. package/lib/module/atoms/box/box.js +3 -0
  9. package/lib/module/atoms/box/box.js.map +1 -1
  10. package/lib/module/atoms/box/index.js +0 -1
  11. package/lib/module/atoms/box/index.js.map +1 -1
  12. package/lib/module/atoms/button/button-tokens.js +2 -2
  13. package/lib/module/atoms/button/button-tokens.js.map +1 -1
  14. package/lib/module/atoms/button/button.android.js +6 -2
  15. package/lib/module/atoms/button/button.android.js.map +1 -1
  16. package/lib/module/atoms/button/button.ios.js +7 -2
  17. package/lib/module/atoms/button/button.ios.js.map +1 -1
  18. package/lib/module/atoms/card/card.android.js +5 -3
  19. package/lib/module/atoms/card/card.android.js.map +1 -1
  20. package/lib/module/atoms/card/card.ios.js +7 -3
  21. package/lib/module/atoms/card/card.ios.js.map +1 -1
  22. package/lib/module/atoms/checkbox/checkbox.android.js +1 -1
  23. package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
  24. package/lib/module/atoms/fab/fab.android.js +9 -1
  25. package/lib/module/atoms/fab/fab.android.js.map +1 -1
  26. package/lib/module/atoms/fab/fab.ios.js +9 -3
  27. package/lib/module/atoms/fab/fab.ios.js.map +1 -1
  28. package/lib/module/atoms/glass-view/glass-view.android.js +1 -1
  29. package/lib/module/atoms/glass-view/glass-view.android.js.map +1 -1
  30. package/lib/module/atoms/h-stack/h-stack.ios.js +5 -3
  31. package/lib/module/atoms/h-stack/h-stack.ios.js.map +1 -1
  32. package/lib/module/atoms/icon-button/icon-button.android.js +9 -0
  33. package/lib/module/atoms/icon-button/icon-button.android.js.map +1 -1
  34. package/lib/module/atoms/icon-button/icon-button.ios.js +9 -2
  35. package/lib/module/atoms/icon-button/icon-button.ios.js.map +1 -1
  36. package/lib/module/atoms/image/image.js +4 -4
  37. package/lib/module/atoms/image/image.js.map +1 -1
  38. package/lib/module/atoms/index.js +1 -0
  39. package/lib/module/atoms/index.js.map +1 -1
  40. package/lib/module/atoms/pressable/pressable.js +75 -27
  41. package/lib/module/atoms/pressable/pressable.js.map +1 -1
  42. package/lib/module/atoms/progress/progress.android.js +7 -0
  43. package/lib/module/atoms/progress/progress.android.js.map +1 -1
  44. package/lib/module/atoms/progress/progress.ios.js +6 -1
  45. package/lib/module/atoms/progress/progress.ios.js.map +1 -1
  46. package/lib/module/atoms/rn-text/rn-text.js +66 -26
  47. package/lib/module/atoms/rn-text/rn-text.js.map +1 -1
  48. package/lib/module/atoms/segmented-control/segmented-control.android.js +1 -1
  49. package/lib/module/atoms/segmented-control/segmented-control.android.js.map +1 -1
  50. package/lib/module/atoms/segmented-control/segmented-control.ios.js +1 -1
  51. package/lib/module/atoms/segmented-control/segmented-control.ios.js.map +1 -1
  52. package/lib/module/atoms/skeleton/skeleton.js +22 -8
  53. package/lib/module/atoms/skeleton/skeleton.js.map +1 -1
  54. package/lib/module/atoms/stagger/index.js +5 -0
  55. package/lib/module/atoms/stagger/index.js.map +1 -0
  56. package/lib/module/atoms/stagger/stagger.js +102 -0
  57. package/lib/module/atoms/stagger/stagger.js.map +1 -0
  58. package/lib/module/atoms/stagger/stagger.types.js +4 -0
  59. package/lib/module/atoms/stagger/stagger.types.js.map +1 -0
  60. package/lib/module/atoms/surface/surface.android.js +5 -3
  61. package/lib/module/atoms/surface/surface.android.js.map +1 -1
  62. package/lib/module/atoms/surface/surface.ios.js +7 -3
  63. package/lib/module/atoms/surface/surface.ios.js.map +1 -1
  64. package/lib/module/atoms/switch/switch.android.js +1 -1
  65. package/lib/module/atoms/switch/switch.android.js.map +1 -1
  66. package/lib/module/atoms/text/text.android.js +5 -3
  67. package/lib/module/atoms/text/text.android.js.map +1 -1
  68. package/lib/module/atoms/text/text.ios.js +6 -4
  69. package/lib/module/atoms/text/text.ios.js.map +1 -1
  70. package/lib/module/atoms/v-stack/v-stack.ios.js +5 -3
  71. package/lib/module/atoms/v-stack/v-stack.ios.js.map +1 -1
  72. package/lib/module/atoms/z-stack/z-stack.ios.js +5 -3
  73. package/lib/module/atoms/z-stack/z-stack.ios.js.map +1 -1
  74. package/lib/module/glyph/glyph.android.js +2 -0
  75. package/lib/module/glyph/glyph.android.js.map +1 -1
  76. package/lib/module/glyph/glyph.ios.js +5 -2
  77. package/lib/module/glyph/glyph.ios.js.map +1 -1
  78. package/lib/module/molecules/collapsible/collapsible.android.js +18 -8
  79. package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
  80. package/lib/module/molecules/labeled-content/labeled-content.js +4 -1
  81. package/lib/module/molecules/labeled-content/labeled-content.js.map +1 -1
  82. package/lib/module/molecules/link/link.js +4 -1
  83. package/lib/module/molecules/link/link.js.map +1 -1
  84. package/lib/module/molecules/list-item/list-item.js +4 -1
  85. package/lib/module/molecules/list-item/list-item.js.map +1 -1
  86. package/lib/module/molecules/navigation-bar/navigation-bar.android.js +1 -1
  87. package/lib/module/molecules/navigation-bar/navigation-bar.android.js.map +1 -1
  88. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js +1 -1
  89. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js.map +1 -1
  90. package/lib/module/molecules/rn-list-item/rn-list-item.js +10 -2
  91. package/lib/module/molecules/rn-list-item/rn-list-item.js.map +1 -1
  92. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js +5 -6
  93. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js.map +1 -1
  94. package/lib/module/molecules/stepper/stepper.ios.js +6 -2
  95. package/lib/module/molecules/stepper/stepper.ios.js.map +1 -1
  96. package/lib/module/organisms/fab-menu/fab-menu.android.js +8 -5
  97. package/lib/module/organisms/fab-menu/fab-menu.android.js.map +1 -1
  98. package/lib/module/organisms/fab-menu/fab-menu.ios.js +5 -2
  99. package/lib/module/organisms/fab-menu/fab-menu.ios.js.map +1 -1
  100. package/lib/module/organisms/snackbar/snackbar.js +155 -18
  101. package/lib/module/organisms/snackbar/snackbar.js.map +1 -1
  102. package/lib/module/organisms/swipe-actions/swipe-actions.android.js +1 -1
  103. package/lib/module/organisms/swipe-actions/swipe-actions.android.js.map +1 -1
  104. package/lib/module/use-stack-modifiers.js +10 -8
  105. package/lib/module/use-stack-modifiers.js.map +1 -1
  106. package/lib/typescript/atoms/activity-indicator/activity-indicator.types.d.ts +2 -0
  107. package/lib/typescript/atoms/avatar/avatar.types.d.ts +5 -0
  108. package/lib/typescript/atoms/box/box.d.ts +3 -0
  109. package/lib/typescript/atoms/box/index.d.ts +0 -1
  110. package/lib/typescript/atoms/button/button-tokens.d.ts +1 -1
  111. package/lib/typescript/atoms/button/button.types.d.ts +2 -0
  112. package/lib/typescript/atoms/card/card.types.d.ts +2 -2
  113. package/lib/typescript/atoms/fab/fab.types.d.ts +8 -1
  114. package/lib/typescript/atoms/h-stack/h-stack.types.d.ts +6 -3
  115. package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +7 -0
  116. package/lib/typescript/atoms/image/image.d.ts +2 -2
  117. package/lib/typescript/atoms/index.d.ts +1 -0
  118. package/lib/typescript/atoms/pressable/pressable.types.d.ts +10 -9
  119. package/lib/typescript/atoms/progress/progress.types.d.ts +2 -0
  120. package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +7 -4
  121. package/lib/typescript/atoms/skeleton/skeleton.types.d.ts +3 -2
  122. package/lib/typescript/atoms/stagger/index.d.ts +2 -0
  123. package/lib/typescript/atoms/stagger/stagger.d.ts +2 -0
  124. package/lib/typescript/atoms/stagger/stagger.types.d.ts +32 -0
  125. package/lib/typescript/atoms/surface/surface.types.d.ts +2 -2
  126. package/lib/typescript/atoms/text/text.types.d.ts +2 -2
  127. package/lib/typescript/atoms/v-stack/v-stack.types.d.ts +3 -3
  128. package/lib/typescript/atoms/z-stack/z-stack.types.d.ts +3 -3
  129. package/lib/typescript/glyph/glyph.types.d.ts +2 -0
  130. package/lib/typescript/molecules/labeled-content/labeled-content.types.d.ts +4 -2
  131. package/lib/typescript/molecules/link/link.types.d.ts +2 -1
  132. package/lib/typescript/molecules/list-item/list-item.types.d.ts +5 -3
  133. package/lib/typescript/molecules/stepper/stepper.ios.d.ts +1 -1
  134. package/lib/typescript/organisms/fab-menu/fab-menu.types.d.ts +5 -0
  135. package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +35 -4
  136. package/lib/typescript/use-stack-modifiers.d.ts +6 -5
  137. package/package.json +4 -4
  138. package/src/atoms/activity-indicator/activity-indicator.android.tsx +7 -1
  139. package/src/atoms/activity-indicator/activity-indicator.ios.tsx +9 -1
  140. package/src/atoms/activity-indicator/activity-indicator.types.ts +2 -0
  141. package/src/atoms/avatar/avatar.tsx +5 -1
  142. package/src/atoms/avatar/avatar.types.ts +5 -0
  143. package/src/atoms/box/box.tsx +3 -0
  144. package/src/atoms/box/index.ts +0 -1
  145. package/src/atoms/button/button-tokens.ts +2 -2
  146. package/src/atoms/button/button.android.tsx +5 -3
  147. package/src/atoms/button/button.ios.tsx +10 -2
  148. package/src/atoms/button/button.types.ts +2 -0
  149. package/src/atoms/card/card.android.tsx +6 -4
  150. package/src/atoms/card/card.ios.tsx +7 -3
  151. package/src/atoms/card/card.types.ts +8 -7
  152. package/src/atoms/checkbox/checkbox.android.tsx +1 -1
  153. package/src/atoms/fab/fab.android.tsx +5 -3
  154. package/src/atoms/fab/fab.ios.tsx +8 -4
  155. package/src/atoms/fab/fab.types.ts +8 -1
  156. package/src/atoms/glass-view/glass-view.android.tsx +1 -1
  157. package/src/atoms/h-stack/h-stack.ios.tsx +6 -4
  158. package/src/atoms/h-stack/h-stack.types.ts +17 -13
  159. package/src/atoms/icon-button/icon-button.android.tsx +16 -3
  160. package/src/atoms/icon-button/icon-button.ios.tsx +13 -4
  161. package/src/atoms/icon-button/icon-button.types.ts +7 -0
  162. package/src/atoms/image/image.tsx +3 -3
  163. package/src/atoms/index.ts +1 -0
  164. package/src/atoms/pressable/pressable.tsx +164 -100
  165. package/src/atoms/pressable/pressable.types.ts +32 -29
  166. package/src/atoms/progress/progress.android.tsx +5 -3
  167. package/src/atoms/progress/progress.ios.tsx +5 -2
  168. package/src/atoms/progress/progress.types.ts +2 -0
  169. package/src/atoms/rn-text/rn-text.tsx +101 -32
  170. package/src/atoms/rn-text/rn-text.types.ts +7 -3
  171. package/src/atoms/segmented-control/segmented-control.android.tsx +1 -1
  172. package/src/atoms/segmented-control/segmented-control.ios.tsx +1 -1
  173. package/src/atoms/skeleton/skeleton.tsx +20 -6
  174. package/src/atoms/skeleton/skeleton.types.ts +3 -2
  175. package/src/atoms/stagger/index.ts +2 -0
  176. package/src/atoms/stagger/stagger.tsx +98 -0
  177. package/src/atoms/stagger/stagger.types.ts +35 -0
  178. package/src/atoms/surface/surface.android.tsx +6 -4
  179. package/src/atoms/surface/surface.ios.tsx +7 -3
  180. package/src/atoms/surface/surface.types.ts +10 -9
  181. package/src/atoms/switch/switch.android.tsx +1 -1
  182. package/src/atoms/text/text.android.tsx +6 -4
  183. package/src/atoms/text/text.ios.tsx +6 -4
  184. package/src/atoms/text/text.types.ts +23 -22
  185. package/src/atoms/v-stack/v-stack.ios.tsx +6 -4
  186. package/src/atoms/v-stack/v-stack.types.ts +17 -16
  187. package/src/atoms/z-stack/z-stack.ios.tsx +6 -4
  188. package/src/atoms/z-stack/z-stack.types.ts +15 -14
  189. package/src/glyph/glyph.android.tsx +14 -2
  190. package/src/glyph/glyph.ios.tsx +13 -3
  191. package/src/glyph/glyph.types.ts +2 -0
  192. package/src/molecules/collapsible/collapsible.android.tsx +27 -7
  193. package/src/molecules/labeled-content/labeled-content.tsx +12 -1
  194. package/src/molecules/labeled-content/labeled-content.types.ts +4 -2
  195. package/src/molecules/link/link.tsx +2 -1
  196. package/src/molecules/link/link.types.ts +2 -1
  197. package/src/molecules/list-item/list-item.tsx +20 -2
  198. package/src/molecules/list-item/list-item.types.ts +5 -3
  199. package/src/molecules/navigation-bar/navigation-bar.android.tsx +1 -1
  200. package/src/molecules/navigation-bar/navigation-bar.ios.tsx +1 -1
  201. package/src/molecules/rn-list-item/rn-list-item.tsx +18 -3
  202. package/src/molecules/scroll-edge-container/scroll-edge-container.android.tsx +5 -4
  203. package/src/molecules/stepper/stepper.ios.tsx +8 -2
  204. package/src/organisms/fab-menu/fab-menu.android.tsx +9 -5
  205. package/src/organisms/fab-menu/fab-menu.ios.tsx +6 -2
  206. package/src/organisms/fab-menu/fab-menu.types.ts +5 -0
  207. package/src/organisms/snackbar/snackbar.tsx +125 -21
  208. package/src/organisms/snackbar/snackbar.types.ts +38 -4
  209. package/src/organisms/swipe-actions/swipe-actions.android.tsx +1 -1
  210. package/src/use-stack-modifiers.ts +20 -7
  211. package/lib/module/atoms/box/a-box.js +0 -6
  212. package/lib/module/atoms/box/a-box.js.map +0 -1
  213. package/lib/typescript/atoms/box/a-box.d.ts +0 -49
  214. package/src/atoms/box/a-box.ts +0 -5
@@ -1,8 +1,9 @@
1
1
  import { type ModifierConfig } from '@expo/ui/jetpack-compose/modifiers';
2
- import { type ResponsiveSx } from '@hzblj/kara-sx';
2
+ import { type AccessibilityProps, type ResponsiveSx } from '@hzblj/kara-sx';
3
3
  /**
4
- * A Compose stack's modifiers in the order a pressable one needs: its frame, shape and background, then the press,
5
- * then its padding. Compose clips a `clickable` to what comes after it, so with the padding first the ripple would stop
6
- * at the padding and a row pressed by its edge would light up only in the middle.
4
+ * A Compose stack's modifiers in the order a pressable one needs: its frame, shape and background, then the press, its
5
+ * semantics, then its padding. Compose clips a `clickable` to what comes after it, so with the padding first the ripple
6
+ * would stop at the padding and a row pressed by its edge would light up only in the middle. The semantics come after
7
+ * the click, because a label that replaces the children's clears whatever semantics are inside it.
7
8
  */
8
- export declare const useStackModifiers: (sx: ResponsiveSx, onPress?: () => void, testID?: string) => ModifierConfig[];
9
+ export declare const useStackModifiers: (props: ResponsiveSx & AccessibilityProps, onPress?: () => void, testID?: string) => ModifierConfig[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzblj/kara-components",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "The atoms, molecules, organisms and templates of @hzblj/kara-ui — real SwiftUI and Jetpack Compose views through @expo/ui.",
5
5
  "author": "Jan Blazej",
6
6
  "license": "MIT",
@@ -59,9 +59,9 @@
59
59
  ]
60
60
  },
61
61
  "dependencies": {
62
- "@hzblj/kara-native": "^0.10.0",
63
- "@hzblj/kara-sx": "^0.10.0",
64
- "@hzblj/kara-theme": "^0.10.0",
62
+ "@hzblj/kara-native": "^0.12.0",
63
+ "@hzblj/kara-sx": "^0.12.0",
64
+ "@hzblj/kara-theme": "^0.12.0",
65
65
  "sf-symbols-typescript": "^2.2.0"
66
66
  },
67
67
  "peerDependencies": {
@@ -1,5 +1,6 @@
1
1
  import {Host, LoadingIndicator} from '@expo/ui/jetpack-compose'
2
2
  import {fillMaxSize, testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
3
+ import {useAccessibilityModifiers} from '@hzblj/kara-sx'
3
4
  import {useAppTheme} from '@hzblj/kara-theme'
4
5
  import {useMemo} from 'react'
5
6
  import {StyleSheet} from 'react-native'
@@ -17,12 +18,17 @@ const styles = StyleSheet.create({
17
18
  * anywhere and takes its place in the first frame.
18
19
  */
19
20
  export const ActivityIndicator: ActivityIndicatorComponent = ({
21
+ accessibilityLabel,
20
22
  color = 'fill.accent.primary',
21
23
  size = 'small',
22
24
  testID,
23
25
  }: ActivityIndicatorProps) => {
24
26
  const theme = useAppTheme()
25
- const modifiers = useMemo(() => (testID ? [fillMaxSize(), testIdModifier(testID)] : [fillMaxSize()]), [testID])
27
+ const accessibilityModifiers = useAccessibilityModifiers({accessibilityLabel})
28
+ const modifiers = useMemo(
29
+ () => [fillMaxSize(), ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers],
30
+ [accessibilityModifiers, testID]
31
+ )
26
32
 
27
33
  return (
28
34
  <Host colorScheme={theme.dark ? 'dark' : 'light'} seedColor={theme.materialSeed} style={styles[size]}>
@@ -5,11 +5,19 @@ import type {ActivityIndicatorComponent, ActivityIndicatorProps} from './activit
5
5
 
6
6
  /** React Native's spinner, which on iOS is UIKit's own `UIActivityIndicatorView`. */
7
7
  export const ActivityIndicator: ActivityIndicatorComponent = ({
8
+ accessibilityLabel,
8
9
  color = 'fill.accent.primary',
9
10
  size = 'small',
10
11
  testID,
11
12
  }: ActivityIndicatorProps) => {
12
13
  const theme = useAppTheme()
13
14
 
14
- return <RNActivityIndicator color={theme.color(color)} size={size} testID={testID} />
15
+ return (
16
+ <RNActivityIndicator
17
+ accessibilityLabel={accessibilityLabel}
18
+ color={theme.color(color)}
19
+ size={size}
20
+ testID={testID}
21
+ />
22
+ )
15
23
  }
@@ -9,6 +9,8 @@ export type ActivityIndicatorSize = 'large' | 'small'
9
9
  * Android, so it renders anywhere. For how far along something is, use `Progress`.
10
10
  */
11
11
  export type ActivityIndicatorProps = {
12
+ /** What is loading, for VoiceOver and TalkBack: "Loading photos". Unset, the platform's own word ("In progress"). */
13
+ accessibilityLabel?: string
12
14
  /** Its colour. `fill.accent.primary` by default. */
13
15
  color?: ColorToken
14
16
  /** `small` sits in a row or a button, `large` holds a screen that is loading. `small` by default. */
@@ -17,11 +17,15 @@ const VARIANT = {
17
17
  sm: 'caption',
18
18
  } as const
19
19
 
20
- export const Avatar: AvatarComponent = ({initials, size = 'md', source, testID}: AvatarProps) => {
20
+ export const Avatar: AvatarComponent = ({accessibilityLabel, initials, size = 'md', source, testID}: AvatarProps) => {
21
21
  const diameter = DIAMETER[size]
22
22
 
23
23
  return (
24
24
  <Box
25
+ aria-hidden={accessibilityLabel === undefined}
26
+ accessibilityLabel={accessibilityLabel}
27
+ accessibilityRole={accessibilityLabel === undefined ? undefined : 'image'}
28
+ accessible={accessibilityLabel !== undefined}
25
29
  align="center"
26
30
  bg="fill.tertiary"
27
31
  borderRadius="full"
@@ -5,6 +5,11 @@ export type AvatarSize = 'lg' | 'md' | 'sm'
5
5
 
6
6
  /** A round avatar: the image in `source`, or `initials` without one. The React Native tier: it needs no host. */
7
7
  export type AvatarProps = {
8
+ /**
9
+ * Who it is, for VoiceOver and TalkBack: "Jan Blazej". Unset, the avatar is decorative and hidden from them, as one
10
+ * beside the person's name should be.
11
+ */
12
+ accessibilityLabel?: string
8
13
  /** Shown in place of the image when there is no `source`. */
9
14
  initials?: string
10
15
  /** The diameter: `sm` is `s24`, `md` `s40`, `lg` `s64`. `md` by default. */
@@ -4,5 +4,8 @@ import {View} from 'react-native'
4
4
  /**
5
5
  * A React Native `View` that takes the design system's `sx` props. The `sx()` HOC is the whole
6
6
  * implementation — Box is just its first caller, and any other style-taking component gets the same.
7
+ *
8
+ * That includes motion: `entering`, `exiting`, `layout`, `animate` or an animated `style` make it an `Animated.View`
9
+ * from its first frame, and without them it stays a plain `View`.
7
10
  */
8
11
  export const Box = sx(View)
@@ -1,3 +1,2 @@
1
- export * from './a-box'
2
1
  export * from './box'
3
2
  export * from './box.types'
@@ -9,7 +9,7 @@ type ButtonTokens = {
9
9
  }
10
10
 
11
11
  const TOKENS = {
12
- accent: {content: 'content.on.bg', fill: 'fill.accent.primary', shadow: 'btn.accent'},
12
+ accent: {content: 'content.on.accent', fill: 'fill.accent.primary', shadow: 'btn.accent'},
13
13
  destructive: {content: 'content.on.destructive', fill: 'fill.destructive.primary', shadow: 'btn.destructive'},
14
14
  glass: {content: 'content.primary', fill: 'btn.bg.glass.base', shadow: 'btn.glass'},
15
15
  secondary: {content: 'content.primary', fill: 'btn.bg.secondary.base', shadow: 'btn.secondary'},
@@ -81,4 +81,4 @@ const currentHex = (token: ColorToken, theme: Theme): unknown => {
81
81
  * content colour over a pale one (`fill.accent.base`) — the light container tokens pair with it in both appearances.
82
82
  */
83
83
  export const contentOnTint = (tint: ColorToken, theme: Theme): ColorToken =>
84
- (luminance(currentHex(tint, theme)) ?? 0) > 0.4 ? 'content.primary' : 'content.on.bg'
84
+ (luminance(currentHex(tint, theme)) ?? 0) > 0.4 ? 'content.primary' : 'content.on.accent'
@@ -1,6 +1,6 @@
1
1
  import {Button as ComposeButton, Text as ComposeText, OutlinedButton} 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 {useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
4
  import {useAppTheme} from '@hzblj/kara-theme'
5
5
  import {useMemo} from 'react'
6
6
  import {useHostGuard} from '../../host-context'
@@ -16,6 +16,7 @@ import {buttonTokens, contentOnTint} from './button-tokens'
16
16
  * colour, as a prominent style does on iOS. `buttonStyle` and `controlSize` are SwiftUI's alone.
17
17
  */
18
18
  export const Button: ButtonComponent = ({
19
+ accessibilityHint,
19
20
  disabled = false,
20
21
  label,
21
22
  onPress,
@@ -28,9 +29,10 @@ export const Button: ButtonComponent = ({
28
29
  const theme = useAppTheme()
29
30
  const tokens = buttonTokens(variant, disabled, theme)
30
31
  const sxModifiers = useSxModifiers({wide})
32
+ const accessibilityModifiers = useAccessibilityModifiers({accessibilityHint}, accessibilityHint !== undefined)
31
33
  const modifiers = useMemo(
32
- () => (testID ? [...sxModifiers, testIdModifier(testID)] : sxModifiers),
33
- [sxModifiers, testID]
34
+ () => [...sxModifiers, ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers],
35
+ [accessibilityModifiers, sxModifiers, testID]
34
36
  )
35
37
  const style = theme.typography.body
36
38
  const outlined = variant === 'glass' && !tint
@@ -6,6 +6,7 @@ import {
6
6
  glassEffect as glassEffectModifier,
7
7
  tint as tintModifier,
8
8
  } from '@expo/ui/swift-ui/modifiers'
9
+ import {useAccessibilityModifiers} from '@hzblj/kara-sx'
9
10
  import {type ColorToken, useAppTheme} from '@hzblj/kara-theme'
10
11
  import {useMemo} from 'react'
11
12
  import {useHostGuard} from '../../host-context'
@@ -29,6 +30,7 @@ const TINT_BY_VARIANT = {
29
30
  } as const satisfies Record<ButtonVariant, ColorToken | undefined>
30
31
 
31
32
  export const Button: ButtonComponent = ({
33
+ accessibilityHint,
32
34
  buttonStyle,
33
35
  controlSize = 'large',
34
36
  disabled = false,
@@ -59,8 +61,14 @@ export const Button: ButtonComponent = ({
59
61
  // A prominent style — and tinted glass — is coloured by the tint, so a caller's own tint picks the label on it.
60
62
  const content = tint && filled && !disabled ? contentOnTint(tint, theme) : tokens.content
61
63
 
64
+ const accessibilityModifiers = useAccessibilityModifiers({accessibilityHint})
65
+
62
66
  const modifiers = useMemo(() => {
63
- const list = [buttonStyleModifier(glassTint ? 'plain' : style), controlSizeModifier(controlSize)]
67
+ const list = [
68
+ buttonStyleModifier(glassTint ? 'plain' : style),
69
+ controlSizeModifier(controlSize),
70
+ ...accessibilityModifiers,
71
+ ]
64
72
 
65
73
  if (glassTint) {
66
74
  list.push(
@@ -75,7 +83,7 @@ export const Button: ButtonComponent = ({
75
83
  }
76
84
 
77
85
  return list
78
- }, [controlSize, disabled, glassTint, resolvedTint, style, theme])
86
+ }, [accessibilityModifiers, controlSize, disabled, glassTint, resolvedTint, style, theme])
79
87
 
80
88
  return (
81
89
  <SwiftUIButton modifiers={modifiers} onPress={onPress} testID={testID}>
@@ -24,6 +24,8 @@ export type ButtonControlSize = 'extraLarge' | 'large' | 'mini' | 'regular' | 's
24
24
 
25
25
  /** A text button: SwiftUI `Button` on iOS, Material's filled button (outlined for `glass`) on Android. */
26
26
  export type ButtonProps = {
27
+ /** What pressing it does, when the label alone does not say. See `AccessibilityProps`. */
28
+ accessibilityHint?: string
27
29
  /**
28
30
  * Replaces the style the variant picks: `borderedProminent` for `accent` and `destructive`, `bordered` for
29
31
  * `secondary`, `glass` for `glass`. iOS only. Android ignores it.
@@ -1,16 +1,18 @@
1
1
  import {Column} 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 {useMemo} from 'react'
5
5
  import {useHostGuard} from '../../host-context'
6
6
  import type {CardComponent, CardProps} from './card.types'
7
7
 
8
- export const Card: CardComponent = ({children, testID, ...sx}: CardProps) => {
8
+ export const Card: CardComponent = ({children, testID, ...props}: CardProps) => {
9
9
  useHostGuard('Card')
10
+ const [accessibility, sx] = splitAccessibility(props)
10
11
  const sxModifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', shadow: 'card', ...sx})
12
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility)
11
13
  const modifiers = useMemo(
12
- () => (testID ? [...sxModifiers, testIdModifier(testID)] : sxModifiers),
13
- [sxModifiers, testID]
14
+ () => [...sxModifiers, ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers],
15
+ [accessibilityModifiers, sxModifiers, testID]
14
16
  )
15
17
 
16
18
  return <Column modifiers={modifiers}>{children}</Column>
@@ -1,11 +1,15 @@
1
1
  import {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 {CardComponent, CardProps} from './card.types'
5
6
 
6
- export const Card: CardComponent = ({children, testID, ...sx}: CardProps) => {
7
+ export const Card: CardComponent = ({children, testID, ...props}: CardProps) => {
7
8
  useHostGuard('Card')
8
- const modifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', shadow: 'card', ...sx})
9
+ const [accessibility, sx] = splitAccessibility(props)
10
+ const sxModifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', shadow: 'card', ...sx})
11
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility)
12
+ const modifiers = useMemo(() => [...sxModifiers, ...accessibilityModifiers], [accessibilityModifiers, sxModifiers])
9
13
 
10
14
  return (
11
15
  <VStack alignment="leading" modifiers={modifiers} testID={testID}>
@@ -1,15 +1,16 @@
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
5
  * A raised container: SwiftUI `VStack` / Compose `Column` on `surface.primary`, with a large radius, `s16` padding and
6
6
  * the `card` shadow. Any `sx` prop overrides its default.
7
7
  */
8
- export type CardProps = ResponsiveSx & {
9
- /** Native views, stacked vertically from the leading edge. */
10
- children?: ReactNode
11
- /** Test identifier, forwarded to the native view. */
12
- testID?: string
13
- }
8
+ export type CardProps = ResponsiveSx &
9
+ AccessibilityProps & {
10
+ /** Native views, stacked vertically from the leading edge. */
11
+ children?: ReactNode
12
+ /** Test identifier, forwarded to the native view. */
13
+ testID?: string
14
+ }
14
15
 
15
16
  export type CardComponent = (props: CardProps) => ReactNode
@@ -23,7 +23,7 @@ export const Checkbox: CheckboxComponent = ({disabled = false, label, onValueCha
23
23
  )
24
24
  const control = (
25
25
  <ComposeCheckbox
26
- colors={{checkedColor: theme.color('fill.accent.primary'), checkmarkColor: theme.color('content.on.bg')}}
26
+ colors={{checkedColor: theme.color('fill.accent.primary'), checkmarkColor: theme.color('content.on.accent')}}
27
27
  enabled={!disabled}
28
28
  onCheckedChange={onValueChange ?? NOOP}
29
29
  value={value}
@@ -1,20 +1,22 @@
1
1
  import {FloatingActionButton} 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 {Icon} from '../icon'
6
7
  import type {FabComponent, FabProps} from './fab.types'
7
8
 
8
- export const Fab: FabComponent = ({name, onPress}: FabProps) => {
9
+ export const Fab: FabComponent = ({accessibilityHint, accessibilityLabel, name, onPress}: FabProps) => {
9
10
  useHostGuard('Fab')
10
11
 
11
12
  const theme = useAppTheme()
13
+ const modifiers = useAccessibilityModifiers({accessibilityHint}, accessibilityHint !== undefined)
12
14
 
13
15
  return (
14
- <FloatingActionButton containerColor={theme.color('fill.accent.primary')} onClick={onPress}>
16
+ <FloatingActionButton containerColor={theme.color('fill.accent.primary')} modifiers={modifiers} onClick={onPress}>
15
17
  {/* `@expo/ui`'s FAB is slot-based: content outside `.Icon` is not composed at all. */}
16
18
  <FloatingActionButton.Icon>
17
- <Icon color="content.on.bg" name={name} />
19
+ <Icon accessibilityLabel={accessibilityLabel} color="content.on.accent" name={name} />
18
20
  </FloatingActionButton.Icon>
19
21
  </FloatingActionButton>
20
22
  )
@@ -1,13 +1,14 @@
1
1
  import {Button} from '@expo/ui/swift-ui'
2
2
  import {buttonStyle} from '@expo/ui/swift-ui/modifiers'
3
- import {useSxModifiers} from '@hzblj/kara-sx'
3
+ import {useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
4
  import {useMemo} from 'react'
5
5
  import {useHostGuard} from '../../host-context'
6
6
  import {Icon} from '../icon'
7
7
  import type {FabComponent, FabProps} from './fab.types'
8
8
 
9
- export const Fab: FabComponent = ({name, onPress, testID}: FabProps) => {
9
+ export const Fab: FabComponent = ({accessibilityHint, accessibilityLabel, name, onPress, testID}: FabProps) => {
10
10
  useHostGuard('Fab')
11
+ const accessibilityModifiers = useAccessibilityModifiers({accessibilityHint})
11
12
 
12
13
  const sxModifiers = useSxModifiers({
13
14
  bg: 'fill.accent.primary',
@@ -17,11 +18,14 @@ export const Fab: FabComponent = ({name, onPress, testID}: FabProps) => {
17
18
  shadow: 'btn.accent',
18
19
  w: 56,
19
20
  })
20
- const modifiers = useMemo(() => [buttonStyle('plain'), ...sxModifiers], [sxModifiers])
21
+ const modifiers = useMemo(
22
+ () => [buttonStyle('plain'), ...sxModifiers, ...accessibilityModifiers],
23
+ [accessibilityModifiers, sxModifiers]
24
+ )
21
25
 
22
26
  return (
23
27
  <Button modifiers={modifiers} onPress={onPress} testID={testID}>
24
- <Icon color="content.on.bg" name={name} />
28
+ <Icon accessibilityLabel={accessibilityLabel} color="content.on.accent" name={name} />
25
29
  </Button>
26
30
  )
27
31
  }
@@ -7,7 +7,14 @@ import type {IconName} from '../icon'
7
7
  * `Button`) on iOS. It renders inline; pinning it to a corner is up to the screen.
8
8
  */
9
9
  export type FabProps = {
10
- /** The icon, one of the app's, drawn in `content.on.bg`. */
10
+ /**
11
+ * What VoiceOver and TalkBack say for the button, since an icon has no words of its own: "Share", "Add to
12
+ * favourites". Required for that reason.
13
+ */
14
+ accessibilityLabel: string
15
+ /** What pressing it does, when the label alone does not say. See `AccessibilityProps`. */
16
+ accessibilityHint?: string
17
+ /** The icon, one of the app's, drawn in `content.on.accent`. */
11
18
  name: IconName
12
19
  /** Called when the button is pressed. */
13
20
  onPress?: () => void
@@ -31,7 +31,7 @@ export const GlassView: GlassViewComponent = ({
31
31
  cornerRadius={radii.full}
32
32
  fallbackTint={withOpacity(base, 'o95')}
33
33
  glassTint={withOpacity(base, tint ? 'o85' : 'o50')}
34
- hairlineColor={withOpacity(String(theme.color('content.on.bg')), theme.dark ? 'o20' : 'o60')}
34
+ hairlineColor={withOpacity(String(theme.color('content.on.accent')), theme.dark ? 'o20' : 'o60')}
35
35
  hairlineWidth={1}
36
36
  saturation={1.6}
37
37
  style={style}
@@ -1,6 +1,6 @@
1
1
  import {HStack as SwiftUIHStack} from '@expo/ui/swift-ui'
2
2
  import {onTapGesture} from '@expo/ui/swift-ui/modifiers'
3
- import {type FrameAlignment, useSxModifiers} from '@hzblj/kara-sx'
3
+ import {type FrameAlignment, splitAccessibility, useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
4
  import {spacing} from '@hzblj/kara-theme'
5
5
  import {useMemo} from 'react'
6
6
  import {useHostGuard} from '../../host-context'
@@ -24,13 +24,15 @@ export const HStack: HStackComponent = ({
24
24
  onPress,
25
25
  spacing: gap,
26
26
  testID,
27
- ...sx
27
+ ...props
28
28
  }: HStackProps) => {
29
29
  useHostGuard('HStack')
30
+ const [accessibility, sx] = splitAccessibility(props)
30
31
  const sxModifiers = useSxModifiers(sx, FRAME_ALIGNMENT[align])
32
+ const accessibilityModifiers = useAccessibilityModifiers(accessibility, onPress !== undefined)
31
33
  const modifiers = useMemo(
32
- () => (onPress ? [...sxModifiers, onTapGesture(onPress)] : sxModifiers),
33
- [onPress, sxModifiers]
34
+ () => [...sxModifiers, ...(onPress ? [onTapGesture(onPress)] : []), ...accessibilityModifiers],
35
+ [accessibilityModifiers, onPress, sxModifiers]
34
36
  )
35
37
 
36
38
  return (
@@ -1,21 +1,25 @@
1
- import type {ResponsiveSx} from '@hzblj/kara-sx'
1
+ import type {AccessibilityProps, ResponsiveSx} from '@hzblj/kara-sx'
2
2
  import type {SpaceSize} from '@hzblj/kara-theme'
3
3
  import type {ReactNode} from 'react'
4
4
 
5
5
  export type HStackAlign = 'center' | 'end' | 'start'
6
6
 
7
7
  /** A horizontal stack: SwiftUI `HStack` / Compose `Row`. */
8
- export type HStackProps = ResponsiveSx & {
9
- /** How the children line up vertically: `start` is the top, `end` the bottom. `start` by default. */
10
- align?: HStackAlign
11
- /** Native views, laid out from the leading edge. */
12
- children?: ReactNode
13
- /** Makes the stack pressable: a tap gesture on iOS, a `clickable` with Material's ripple on Android. */
14
- onPress?: () => void
15
- /** The gap between children. Unset, SwiftUI puts its default spacing between them and Compose none. */
16
- spacing?: SpaceSize
17
- /** Test identifier, forwarded to the native view. */
18
- testID?: string
19
- }
8
+ export type HStackProps = ResponsiveSx &
9
+ AccessibilityProps & {
10
+ /** How the children line up vertically: `start` is the top, `end` the bottom. `start` by default. */
11
+ align?: HStackAlign
12
+ /** Native views, laid out from the leading edge. */
13
+ children?: ReactNode
14
+ /**
15
+ * Makes the stack pressable: a tap gesture on iOS, a `clickable` with Material's ripple on Android. Screen readers
16
+ * then read the stack as one button.
17
+ */
18
+ onPress?: () => void
19
+ /** The gap between children. Unset, SwiftUI puts its default spacing between them and Compose none. */
20
+ spacing?: SpaceSize
21
+ /** Test identifier, forwarded to the native view. */
22
+ testID?: string
23
+ }
20
24
 
21
25
  export type HStackComponent = (props: HStackProps) => ReactNode
@@ -1,15 +1,28 @@
1
1
  import {IconButton as ComposeIconButton} from '@expo/ui/jetpack-compose'
2
+ import {useAccessibilityModifiers} from '@hzblj/kara-sx'
2
3
 
3
4
  import {useHostGuard} from '../../host-context'
4
5
  import {Icon} from '../icon'
5
6
  import type {IconButtonComponent, IconButtonProps} from './icon-button.types'
6
7
 
7
- export const IconButton: IconButtonComponent = ({disabled = false, name, onPress}: IconButtonProps) => {
8
+ export const IconButton: IconButtonComponent = ({
9
+ accessibilityHint,
10
+ accessibilityLabel,
11
+ disabled = false,
12
+ name,
13
+ onPress,
14
+ }: IconButtonProps) => {
8
15
  useHostGuard('IconButton')
16
+ // The label rides on the icon, as Material's own icon buttons carry it; the hint labels the click.
17
+ const modifiers = useAccessibilityModifiers({accessibilityHint}, accessibilityHint !== undefined)
9
18
 
10
19
  return (
11
- <ComposeIconButton enabled={!disabled} onClick={onPress}>
12
- <Icon color={disabled ? 'btn.content.disabled' : 'content.primary'} name={name} />
20
+ <ComposeIconButton enabled={!disabled} modifiers={modifiers} onClick={onPress}>
21
+ <Icon
22
+ accessibilityLabel={accessibilityLabel}
23
+ color={disabled ? 'btn.content.disabled' : 'content.primary'}
24
+ name={name}
25
+ />
13
26
  </ComposeIconButton>
14
27
  )
15
28
  }
@@ -1,5 +1,6 @@
1
1
  import {Button} from '@expo/ui/swift-ui'
2
2
  import {buttonStyle, contentShape, disabled as disabledModifier, shapes} from '@expo/ui/swift-ui/modifiers'
3
+ import {useAccessibilityModifiers} from '@hzblj/kara-sx'
3
4
  import {useMemo} from 'react'
4
5
 
5
6
  import {useHostGuard} from '../../host-context'
@@ -13,24 +14,32 @@ const TARGET = 44
13
14
  /** The whole square takes the tap, not only the glyph's own pixels. */
14
15
  const WHOLE_TARGET = {modifiers: [contentShape(shapes.circle())]}
15
16
 
16
- export const IconButton: IconButtonComponent = ({disabled = false, name, onPress, testID}: IconButtonProps) => {
17
+ export const IconButton: IconButtonComponent = ({
18
+ accessibilityHint,
19
+ accessibilityLabel,
20
+ disabled = false,
21
+ name,
22
+ onPress,
23
+ testID,
24
+ }: IconButtonProps) => {
17
25
  useHostGuard('IconButton')
26
+ const accessibilityModifiers = useAccessibilityModifiers({accessibilityHint})
18
27
 
19
28
  const modifiers = useMemo(() => {
20
- const list = [buttonStyle('plain')]
29
+ const list = [buttonStyle('plain'), ...accessibilityModifiers]
21
30
 
22
31
  if (disabled) {
23
32
  list.push(disabledModifier(true))
24
33
  }
25
34
 
26
35
  return list
27
- }, [disabled])
36
+ }, [accessibilityModifiers, disabled])
28
37
 
29
38
  return (
30
39
  <Button modifiers={modifiers} onPress={onPress} testID={testID}>
31
40
  <ZStack h={TARGET} ios={WHOLE_TARGET} w={TARGET}>
32
41
  {/* SwiftUI dims a disabled button's label itself; a disabled colour on top of that dimmed it twice. */}
33
- <Icon color="content.primary" name={name} />
42
+ <Icon accessibilityLabel={accessibilityLabel} color="content.primary" name={name} />
34
43
  </ZStack>
35
44
  </Button>
36
45
  )
@@ -8,6 +8,13 @@ import type {IconName} from '../icon'
8
8
  * itself the same way on both.
9
9
  */
10
10
  export type IconButtonProps = {
11
+ /**
12
+ * What VoiceOver and TalkBack say for the button, since an icon has no words of its own: "Share", "Add to
13
+ * favourites". Required for that reason.
14
+ */
15
+ accessibilityLabel: string
16
+ /** What pressing it does, when the label alone does not say. See `AccessibilityProps`. */
17
+ accessibilityHint?: string
11
18
  /** Dims the icon (iOS's own disabled look, `btn.content.disabled` on Android) and stops the button responding. */
12
19
  disabled?: boolean
13
20
  /** The icon, one of the app's. */
@@ -3,12 +3,12 @@ import {duration} from '@hzblj/kara-theme'
3
3
  import {Image as ExpoImage, type ImageProps as ExpoImageProps} from 'expo-image'
4
4
  import type {Ref} from 'react'
5
5
 
6
- const FADE = {duration: duration.d100, timing: 'linear'} as const
6
+ const FADE = {duration: duration.quick, timing: 'ease-out'} as const
7
7
 
8
8
  const FadingImage = (props: ExpoImageProps & {ref?: Ref<ExpoImage>}) => <ExpoImage transition={FADE} {...props} />
9
9
 
10
10
  /**
11
- * `expo-image` with the `sx` props, the way `Box` is a `View` with them, and the design system's 100 ms linear fade
12
- * already applied, so a loaded image never pops in. An explicit `transition` wins, and so does an explicit `style`.
11
+ * `expo-image` with the `sx` props, the way `Box` is a `View` with them, and the design system's fade (`quick`,
12
+ * eased out) already applied, so a loaded image never pops in. An explicit `transition` wins, and so does an explicit `style`.
13
13
  */
14
14
  export const Image = sx(FadingImage)
@@ -32,6 +32,7 @@ export * from './segmented-control'
32
32
  export * from './skeleton'
33
33
  export * from './slider'
34
34
  export * from './spacer'
35
+ export * from './stagger'
35
36
  export * from './surface'
36
37
  export * from './switch'
37
38
  export * from './text'