@hzblj/kara-components 0.16.0 → 0.18.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 (211) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/lib/module/atoms/badge/badge.android.js +5 -0
  3. package/lib/module/atoms/badge/badge.android.js.map +1 -1
  4. package/lib/module/atoms/badge/badge.ios.js +2 -0
  5. package/lib/module/atoms/badge/badge.ios.js.map +1 -1
  6. package/lib/module/atoms/button/button-tokens.js +8 -0
  7. package/lib/module/atoms/button/button-tokens.js.map +1 -1
  8. package/lib/module/atoms/button/button.android.js +43 -15
  9. package/lib/module/atoms/button/button.android.js.map +1 -1
  10. package/lib/module/atoms/button/button.ios.js +66 -10
  11. package/lib/module/atoms/button/button.ios.js.map +1 -1
  12. package/lib/module/atoms/checkbox/checkbox.android.js +5 -1
  13. package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
  14. package/lib/module/atoms/checkbox/checkbox.ios.js +3 -1
  15. package/lib/module/atoms/checkbox/checkbox.ios.js.map +1 -1
  16. package/lib/module/atoms/color-picker/color-picker.android.js +4 -0
  17. package/lib/module/atoms/color-picker/color-picker.android.js.map +1 -1
  18. package/lib/module/atoms/color-picker/color-picker.ios.js +24 -2
  19. package/lib/module/atoms/color-picker/color-picker.ios.js.map +1 -1
  20. package/lib/module/atoms/date-picker/date-picker.android.js +85 -10
  21. package/lib/module/atoms/date-picker/date-picker.android.js.map +1 -1
  22. package/lib/module/atoms/date-picker/date-picker.ios.js +9 -7
  23. package/lib/module/atoms/date-picker/date-picker.ios.js.map +1 -1
  24. package/lib/module/atoms/divider/divider.android.js +17 -4
  25. package/lib/module/atoms/divider/divider.android.js.map +1 -1
  26. package/lib/module/atoms/divider/divider.ios.js +4 -0
  27. package/lib/module/atoms/divider/divider.ios.js.map +1 -1
  28. package/lib/module/atoms/fab/fab.android.js +64 -8
  29. package/lib/module/atoms/fab/fab.android.js.map +1 -1
  30. package/lib/module/atoms/fab/fab.ios.js +68 -12
  31. package/lib/module/atoms/fab/fab.ios.js.map +1 -1
  32. package/lib/module/atoms/icon-button/icon-button.android.js +48 -5
  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 +60 -10
  35. package/lib/module/atoms/icon-button/icon-button.ios.js.map +1 -1
  36. package/lib/module/atoms/list/list.android.js +32 -4
  37. package/lib/module/atoms/list/list.android.js.map +1 -1
  38. package/lib/module/atoms/list/list.ios.js +15 -5
  39. package/lib/module/atoms/list/list.ios.js.map +1 -1
  40. package/lib/module/atoms/picker/picker.ios.js +27 -15
  41. package/lib/module/atoms/picker/picker.ios.js.map +1 -1
  42. package/lib/module/atoms/progress/progress.android.js +21 -7
  43. package/lib/module/atoms/progress/progress.android.js.map +1 -1
  44. package/lib/module/atoms/progress/progress.ios.js +5 -1
  45. package/lib/module/atoms/progress/progress.ios.js.map +1 -1
  46. package/lib/module/atoms/slider/slider.android.js +16 -2
  47. package/lib/module/atoms/slider/slider.android.js.map +1 -1
  48. package/lib/module/atoms/slider/slider.ios.js +22 -2
  49. package/lib/module/atoms/slider/slider.ios.js.map +1 -1
  50. package/lib/module/atoms/switch/switch.android.js +5 -1
  51. package/lib/module/atoms/switch/switch.android.js.map +1 -1
  52. package/lib/module/atoms/switch/switch.ios.js +3 -1
  53. package/lib/module/atoms/switch/switch.ios.js.map +1 -1
  54. package/lib/module/atoms/text/spans.js +2 -1
  55. package/lib/module/atoms/text/spans.js.map +1 -1
  56. package/lib/module/atoms/text/text.android.js +18 -6
  57. package/lib/module/atoms/text/text.android.js.map +1 -1
  58. package/lib/module/atoms/text/text.ios.js +6 -2
  59. package/lib/module/atoms/text/text.ios.js.map +1 -1
  60. package/lib/module/atoms/text-field/text-field-input.js +14 -0
  61. package/lib/module/atoms/text-field/text-field-input.js.map +1 -0
  62. package/lib/module/atoms/text-field/text-field.android.js +113 -6
  63. package/lib/module/atoms/text-field/text-field.android.js.map +1 -1
  64. package/lib/module/atoms/text-field/text-field.ios.js +75 -3
  65. package/lib/module/atoms/text-field/text-field.ios.js.map +1 -1
  66. package/lib/module/molecules/collapsible/collapsible.android.js +4 -3
  67. package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
  68. package/lib/module/molecules/collapsible/collapsible.ios.js +3 -1
  69. package/lib/module/molecules/collapsible/collapsible.ios.js.map +1 -1
  70. package/lib/module/molecules/radio-group/radio-group.android.js +10 -2
  71. package/lib/module/molecules/radio-group/radio-group.android.js.map +1 -1
  72. package/lib/module/molecules/radio-group/radio-group.ios.js +6 -2
  73. package/lib/module/molecules/radio-group/radio-group.ios.js.map +1 -1
  74. package/lib/module/molecules/search-bar/search-bar.js +9 -0
  75. package/lib/module/molecules/search-bar/search-bar.js.map +1 -1
  76. package/lib/module/molecules/stepper/stepper.android.js +25 -13
  77. package/lib/module/molecules/stepper/stepper.android.js.map +1 -1
  78. package/lib/module/molecules/stepper/stepper.ios.js +12 -6
  79. package/lib/module/molecules/stepper/stepper.ios.js.map +1 -1
  80. package/lib/module/organisms/action-sheet/action-sheet.android.js +16 -8
  81. package/lib/module/organisms/action-sheet/action-sheet.android.js.map +1 -1
  82. package/lib/module/organisms/action-sheet/action-sheet.ios.js +16 -3
  83. package/lib/module/organisms/action-sheet/action-sheet.ios.js.map +1 -1
  84. package/lib/module/organisms/alert/alert.android.js +4 -3
  85. package/lib/module/organisms/alert/alert.android.js.map +1 -1
  86. package/lib/module/organisms/alert/alert.ios.js +3 -2
  87. package/lib/module/organisms/alert/alert.ios.js.map +1 -1
  88. package/lib/module/organisms/context-menu/context-menu.android.js +3 -1
  89. package/lib/module/organisms/context-menu/context-menu.android.js.map +1 -1
  90. package/lib/module/organisms/context-menu/context-menu.ios.js +1 -0
  91. package/lib/module/organisms/context-menu/context-menu.ios.js.map +1 -1
  92. package/lib/module/organisms/fab-menu/fab-menu.android.js +3 -0
  93. package/lib/module/organisms/fab-menu/fab-menu.android.js.map +1 -1
  94. package/lib/module/organisms/fab-menu/fab-menu.ios.js +3 -0
  95. package/lib/module/organisms/fab-menu/fab-menu.ios.js.map +1 -1
  96. package/lib/module/organisms/menu/menu-parts.js +20 -0
  97. package/lib/module/organisms/menu/menu-parts.js.map +1 -0
  98. package/lib/module/organisms/menu/menu.android.js +51 -19
  99. package/lib/module/organisms/menu/menu.android.js.map +1 -1
  100. package/lib/module/organisms/menu/menu.ios.js +71 -8
  101. package/lib/module/organisms/menu/menu.ios.js.map +1 -1
  102. package/lib/module/organisms/sheet/sheet.js +2 -1
  103. package/lib/module/organisms/sheet/sheet.js.map +1 -1
  104. package/lib/typescript/atoms/badge/badge.types.d.ts +2 -0
  105. package/lib/typescript/atoms/button/button-tokens.d.ts +6 -2
  106. package/lib/typescript/atoms/button/button.types.d.ts +19 -5
  107. package/lib/typescript/atoms/checkbox/checkbox.types.d.ts +2 -0
  108. package/lib/typescript/atoms/color-picker/color-picker.types.d.ts +2 -0
  109. package/lib/typescript/atoms/date-picker/date-picker.ios.d.ts +5 -5
  110. package/lib/typescript/atoms/date-picker/date-picker.types.d.ts +30 -7
  111. package/lib/typescript/atoms/divider/divider.ios.d.ts +4 -0
  112. package/lib/typescript/atoms/divider/divider.types.d.ts +13 -2
  113. package/lib/typescript/atoms/fab/fab.types.d.ts +21 -4
  114. package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +23 -4
  115. package/lib/typescript/atoms/list/list.types.d.ts +15 -1
  116. package/lib/typescript/atoms/picker/picker.ios.d.ts +5 -3
  117. package/lib/typescript/atoms/picker/picker.types.d.ts +11 -3
  118. package/lib/typescript/atoms/progress/progress.types.d.ts +13 -2
  119. package/lib/typescript/atoms/slider/slider.ios.d.ts +4 -0
  120. package/lib/typescript/atoms/slider/slider.types.d.ts +7 -0
  121. package/lib/typescript/atoms/switch/switch.types.d.ts +2 -0
  122. package/lib/typescript/atoms/text/spans.d.ts +2 -0
  123. package/lib/typescript/atoms/text/text.types.d.ts +9 -1
  124. package/lib/typescript/atoms/text-field/text-field-input.d.ts +7 -0
  125. package/lib/typescript/atoms/text-field/text-field.types.d.ts +77 -4
  126. package/lib/typescript/molecules/collapsible/collapsible.types.d.ts +2 -0
  127. package/lib/typescript/molecules/radio-group/radio-group.types.d.ts +4 -0
  128. package/lib/typescript/molecules/search-bar/search-bar.types.d.ts +11 -1
  129. package/lib/typescript/molecules/stepper/stepper.ios.d.ts +2 -1
  130. package/lib/typescript/molecules/stepper/stepper.types.d.ts +17 -0
  131. package/lib/typescript/organisms/action-sheet/action-sheet.ios.d.ts +4 -0
  132. package/lib/typescript/organisms/action-sheet/action-sheet.types.d.ts +8 -3
  133. package/lib/typescript/organisms/alert/alert.types.d.ts +8 -3
  134. package/lib/typescript/organisms/menu/menu-parts.d.ts +14 -0
  135. package/lib/typescript/organisms/menu/menu.types.d.ts +35 -7
  136. package/lib/typescript/organisms/sheet/sheet.types.d.ts +7 -1
  137. package/package.json +4 -4
  138. package/src/atoms/badge/badge.android.tsx +9 -2
  139. package/src/atoms/badge/badge.ios.tsx +2 -2
  140. package/src/atoms/badge/badge.types.ts +2 -0
  141. package/src/atoms/button/button-tokens.ts +6 -3
  142. package/src/atoms/button/button.android.tsx +35 -11
  143. package/src/atoms/button/button.ios.tsx +72 -8
  144. package/src/atoms/button/button.types.ts +20 -5
  145. package/src/atoms/checkbox/checkbox.android.tsx +5 -2
  146. package/src/atoms/checkbox/checkbox.ios.tsx +2 -2
  147. package/src/atoms/checkbox/checkbox.types.ts +2 -0
  148. package/src/atoms/color-picker/color-picker.android.tsx +10 -2
  149. package/src/atoms/color-picker/color-picker.ios.tsx +24 -1
  150. package/src/atoms/color-picker/color-picker.types.ts +2 -0
  151. package/src/atoms/date-picker/date-picker.android.tsx +86 -16
  152. package/src/atoms/date-picker/date-picker.ios.tsx +20 -8
  153. package/src/atoms/date-picker/date-picker.types.ts +31 -7
  154. package/src/atoms/divider/divider.android.tsx +14 -3
  155. package/src/atoms/divider/divider.ios.tsx +4 -0
  156. package/src/atoms/divider/divider.types.ts +14 -2
  157. package/src/atoms/fab/fab.android.tsx +61 -11
  158. package/src/atoms/fab/fab.ios.tsx +48 -13
  159. package/src/atoms/fab/fab.types.ts +22 -4
  160. package/src/atoms/icon-button/icon-button.android.tsx +60 -10
  161. package/src/atoms/icon-button/icon-button.ios.tsx +77 -8
  162. package/src/atoms/icon-button/icon-button.types.ts +24 -4
  163. package/src/atoms/list/list.android.tsx +26 -4
  164. package/src/atoms/list/list.ios.tsx +20 -5
  165. package/src/atoms/list/list.types.ts +16 -1
  166. package/src/atoms/picker/picker.ios.tsx +46 -16
  167. package/src/atoms/picker/picker.types.ts +12 -3
  168. package/src/atoms/progress/progress.android.tsx +32 -6
  169. package/src/atoms/progress/progress.ios.tsx +11 -5
  170. package/src/atoms/progress/progress.types.ts +13 -2
  171. package/src/atoms/slider/slider.android.tsx +18 -2
  172. package/src/atoms/slider/slider.ios.tsx +38 -2
  173. package/src/atoms/slider/slider.types.ts +7 -0
  174. package/src/atoms/switch/switch.android.tsx +5 -2
  175. package/src/atoms/switch/switch.ios.tsx +2 -2
  176. package/src/atoms/switch/switch.types.ts +2 -0
  177. package/src/atoms/text/spans.ts +2 -1
  178. package/src/atoms/text/text.android.tsx +20 -7
  179. package/src/atoms/text/text.ios.tsx +7 -0
  180. package/src/atoms/text/text.types.ts +10 -1
  181. package/src/atoms/text-field/text-field-input.ts +17 -0
  182. package/src/atoms/text-field/text-field.android.tsx +132 -9
  183. package/src/atoms/text-field/text-field.ios.tsx +110 -6
  184. package/src/atoms/text-field/text-field.types.ts +98 -4
  185. package/src/molecules/collapsible/collapsible.android.tsx +10 -2
  186. package/src/molecules/collapsible/collapsible.ios.tsx +8 -2
  187. package/src/molecules/collapsible/collapsible.types.ts +2 -0
  188. package/src/molecules/radio-group/radio-group.android.tsx +18 -4
  189. package/src/molecules/radio-group/radio-group.ios.tsx +19 -5
  190. package/src/molecules/radio-group/radio-group.types.ts +4 -0
  191. package/src/molecules/search-bar/search-bar.tsx +9 -0
  192. package/src/molecules/search-bar/search-bar.types.ts +12 -1
  193. package/src/molecules/stepper/stepper.android.tsx +24 -4
  194. package/src/molecules/stepper/stepper.ios.tsx +23 -8
  195. package/src/molecules/stepper/stepper.types.ts +17 -0
  196. package/src/organisms/action-sheet/action-sheet.android.tsx +23 -11
  197. package/src/organisms/action-sheet/action-sheet.ios.tsx +20 -3
  198. package/src/organisms/action-sheet/action-sheet.types.ts +8 -3
  199. package/src/organisms/alert/alert.android.tsx +11 -8
  200. package/src/organisms/alert/alert.ios.tsx +2 -1
  201. package/src/organisms/alert/alert.types.ts +8 -3
  202. package/src/organisms/context-menu/context-menu.android.tsx +3 -2
  203. package/src/organisms/context-menu/context-menu.ios.tsx +1 -0
  204. package/src/organisms/fab-menu/fab-menu.android.tsx +3 -0
  205. package/src/organisms/fab-menu/fab-menu.ios.tsx +9 -1
  206. package/src/organisms/menu/menu-parts.ts +21 -0
  207. package/src/organisms/menu/menu.android.tsx +39 -10
  208. package/src/organisms/menu/menu.ios.tsx +55 -5
  209. package/src/organisms/menu/menu.types.ts +30 -7
  210. package/src/organisms/sheet/sheet.tsx +2 -1
  211. package/src/organisms/sheet/sheet.types.ts +7 -1
@@ -4,36 +4,48 @@ import {Island} from '../../atoms/island'
4
4
  import {Text} from '../../atoms/text'
5
5
  import {VStack} from '../../atoms/v-stack'
6
6
  import {useHostGuard} from '../../host-context'
7
+ import {menuActionColor} from '../menu/menu-parts'
7
8
  import {Sheet} from '../sheet'
8
9
  import type {ActionSheetComponent, ActionSheetProps} from './action-sheet.types'
9
10
 
10
11
  /**
11
- * Material's bottom-sheet menu: the title, then a full-width row per action, its icon leading. The sheet is the React
12
- * Native tier, so the rows — Compose views — sit in an `Island` of their own; rendered straight into the sheet they had
13
- * no host and threw.
12
+ * Material's bottom-sheet menu: the title and the message, then a full-width row per action, its icon leading. The
13
+ * sheet is the React Native tier, so the rows — Compose views — sit in an `Island` of their own; rendered straight into
14
+ * the sheet they had no host and threw. A disabled row is greyed and has no press at all, so it neither ripples nor
15
+ * reads as a button.
14
16
  */
15
- export const ActionSheet: ActionSheetComponent = ({actions, onDismiss, title, visible}: ActionSheetProps) => {
17
+ export const ActionSheet: ActionSheetComponent = ({actions, message, onDismiss, title, visible}: ActionSheetProps) => {
16
18
  useHostGuard('ActionSheet')
17
19
 
18
20
  return (
19
21
  <Sheet onDismiss={onDismiss} visible={visible}>
20
22
  <Island>
21
23
  <VStack pb="s16" wide>
22
- <HStack px="s24" py="s12" wide>
24
+ {/* The message at the title's size, lighter: the iOS dialog's own pairing. */}
25
+ <VStack px="s24" py="s12" spacing="s4" wide>
23
26
  <Text color="content.secondary" variant="labelMedium">
24
27
  {title}
25
28
  </Text>
26
- </HStack>
29
+ {message ? (
30
+ <Text color="content.secondary" variant="label">
31
+ {message}
32
+ </Text>
33
+ ) : null}
34
+ </VStack>
27
35
  {actions.map(action => {
28
- const color = action.destructive ? 'content.destructive' : 'content.primary'
36
+ const color = menuActionColor(action)
29
37
 
30
38
  return (
31
39
  <HStack
32
40
  key={action.label}
33
- onPress={() => {
34
- onDismiss()
35
- action.onPress()
36
- }}
41
+ onPress={
42
+ action.disabled
43
+ ? undefined
44
+ : () => {
45
+ onDismiss()
46
+ action.onPress()
47
+ }
48
+ }
37
49
  px="s24"
38
50
  py="s16"
39
51
  spacing="s16"
@@ -1,5 +1,5 @@
1
- import {Button, ConfirmationDialog, Spacer} from '@expo/ui/swift-ui'
2
- import {frame} from '@expo/ui/swift-ui/modifiers'
1
+ import {Button, ConfirmationDialog, Spacer, Text as SwiftUIText} from '@expo/ui/swift-ui'
2
+ import {disabled, frame} from '@expo/ui/swift-ui/modifiers'
3
3
 
4
4
  import {useHostGuard} from '../../host-context'
5
5
  import type {ActionSheetComponent, ActionSheetProps} from './action-sheet.types'
@@ -11,7 +11,13 @@ import type {ActionSheetComponent, ActionSheetProps} from './action-sheet.types'
11
11
  */
12
12
  const ANCHOR = [frame({height: 0, width: 0})]
13
13
 
14
- export const ActionSheet: ActionSheetComponent = ({actions, onDismiss, title, visible}: ActionSheetProps) => {
14
+ const DISABLED = [disabled(true)]
15
+
16
+ /**
17
+ * `titleVisibility="visible"`: left to `automatic`, SwiftUI drops the title of a dialog presented as an action sheet,
18
+ * and the actions lose what they are about.
19
+ */
20
+ export const ActionSheet: ActionSheetComponent = ({actions, message, onDismiss, title, visible}: ActionSheetProps) => {
15
21
  useHostGuard('ActionSheet')
16
22
 
17
23
  return (
@@ -23,6 +29,7 @@ export const ActionSheet: ActionSheetComponent = ({actions, onDismiss, title, vi
23
29
  }
24
30
  }}
25
31
  title={title}
32
+ titleVisibility="visible"
26
33
  >
27
34
  <ConfirmationDialog.Trigger>
28
35
  <Spacer modifiers={ANCHOR} />
@@ -32,11 +39,21 @@ export const ActionSheet: ActionSheetComponent = ({actions, onDismiss, title, vi
32
39
  <Button
33
40
  key={action.label}
34
41
  label={action.label}
42
+ modifiers={action.disabled ? DISABLED : undefined}
35
43
  onPress={action.onPress}
36
44
  role={action.destructive ? 'destructive' : 'default'}
37
45
  />
38
46
  ))}
39
47
  </ConfirmationDialog.Actions>
48
+ {/*
49
+ A native slot view, which cannot hold a bare string (see `Alert`): the dialog mounts while dismissed, so an
50
+ unwrapped string would crash on mount. Plain SwiftUI `Text`, so the message keeps the system's styling.
51
+ */}
52
+ {message ? (
53
+ <ConfirmationDialog.Message>
54
+ <SwiftUIText>{message}</SwiftUIText>
55
+ </ConfirmationDialog.Message>
56
+ ) : null}
40
57
  </ConfirmationDialog>
41
58
  )
42
59
  }
@@ -4,14 +4,19 @@ import type {MenuAction} from '../menu'
4
4
 
5
5
  /**
6
6
  * A list of actions to pick one from: SwiftUI's confirmation dialog on iOS, a `Sheet` of Material rows (icon and label)
7
- * under the title on Android. A native atom: it renders inside `<Screen>` or `<Island>`.
7
+ * under the title and message on Android. A native atom: it renders inside `<Screen>` or `<Island>`.
8
8
  */
9
9
  export type ActionSheetProps = {
10
- /** The choices, top to bottom. Picking one closes the sheet. */
10
+ /** The choices, top to bottom. Picking one closes the sheet. A `disabled` one stays in the list, greyed. */
11
11
  actions: MenuAction[]
12
+ /**
13
+ * A line under the title saying what the choice is about: "The photo will be removed from all your devices." Unset,
14
+ * the title stands alone.
15
+ */
16
+ message?: string
12
17
  /** The sheet closed (after a pick too), so turn `visible` off. */
13
18
  onDismiss: () => void
14
- /** The heading above the actions. Android always shows it; on iOS the system decides, and may hide it. */
19
+ /** The heading above the actions, shown on both platforms. */
15
20
  title: string
16
21
  /** Presents the sheet while true. */
17
22
  visible: boolean
@@ -7,9 +7,10 @@ import type {AlertComponent, AlertProps} from './alert.types'
7
7
  /**
8
8
  * Material's `AlertDialog`, built from its slots — title, text, confirm and dismiss buttons. Content passed as plain
9
9
  * children has no slot to land in, and the dialog draws empty. The actions mirror the iOS alert: the confirm label
10
- * (in the destructive colour when asked) and Cancel.
10
+ * (in the destructive colour when asked) and the cancel label, whose slot stays empty when it is `null`.
11
11
  */
12
12
  export const Alert: AlertComponent = ({
13
+ cancelLabel = 'Cancel',
13
14
  confirmLabel = 'OK',
14
15
  destructive = false,
15
16
  message,
@@ -62,13 +63,15 @@ export const Alert: AlertComponent = ({
62
63
  </ComposeText>
63
64
  </TextButton>
64
65
  </AlertDialog.ConfirmButton>
65
- <AlertDialog.DismissButton>
66
- <TextButton onClick={onDismiss}>
67
- <ComposeText color={theme.colorString('content.accent')} style={theme.typography.body}>
68
- Cancel
69
- </ComposeText>
70
- </TextButton>
71
- </AlertDialog.DismissButton>
66
+ {cancelLabel === null ? null : (
67
+ <AlertDialog.DismissButton>
68
+ <TextButton onClick={onDismiss}>
69
+ <ComposeText color={theme.colorString('content.accent')} style={theme.typography.body}>
70
+ {cancelLabel}
71
+ </ComposeText>
72
+ </TextButton>
73
+ </AlertDialog.DismissButton>
74
+ )}
72
75
  </AlertDialog>
73
76
  )
74
77
  }
@@ -11,6 +11,7 @@ import type {AlertComponent, AlertProps} from './alert.types'
11
11
  const ANCHOR = [frame({height: 0, width: 0})]
12
12
 
13
13
  export const Alert: AlertComponent = ({
14
+ cancelLabel = 'Cancel',
14
15
  confirmLabel = 'OK',
15
16
  destructive = false,
16
17
  message,
@@ -36,7 +37,7 @@ export const Alert: AlertComponent = ({
36
37
  </SwiftUIAlert.Trigger>
37
38
  <SwiftUIAlert.Actions>
38
39
  <Button label={confirmLabel} onPress={onConfirm ?? onDismiss} role={destructive ? 'destructive' : 'default'} />
39
- <Button label="Cancel" onPress={onDismiss} role="cancel" />
40
+ {cancelLabel === null ? null : <Button label={cancelLabel} onPress={onDismiss} role="cancel" />}
40
41
  </SwiftUIAlert.Actions>
41
42
  {/*
42
43
  `Alert.Message` is a native slot view, and a SwiftUI host cannot render a bare string — it
@@ -1,10 +1,15 @@
1
1
  import type {ReactNode} from 'react'
2
2
 
3
3
  /**
4
- * A modal alert with a confirm button and Cancel: SwiftUI's alert on iOS, Material's `AlertDialog` on Android. A native
5
- * atom: it renders inside `<Screen>` or `<Island>`.
4
+ * A modal alert with a confirm button and a cancel button (or the confirm button alone): SwiftUI's alert on iOS,
5
+ * Material's `AlertDialog` on Android. A native atom: it renders inside `<Screen>` or `<Island>`.
6
6
  */
7
7
  export type AlertProps = {
8
+ /**
9
+ * The cancel button's label, in the app's language: "Zrušit". `Cancel` by default. `null` leaves the button out, for
10
+ * an alert that only informs and is closed by its confirm button alone ("OK").
11
+ */
12
+ cancelLabel?: string | null
8
13
  /** The confirm button's label. `OK` by default. */
9
14
  confirmLabel?: string
10
15
  /** Marks the confirm button destructive: the system's destructive role on iOS, the destructive colour on Android. */
@@ -13,7 +18,7 @@ export type AlertProps = {
13
18
  message?: string
14
19
  /** The confirm button was pressed. `onDismiss` follows, as it does for every way the alert closes. */
15
20
  onConfirm?: () => void
16
- /** The alert closed (either button, or on Android a tap outside or back), so turn `visible` off. */
21
+ /** The alert closed (any of its buttons, or on Android a tap outside or back), so turn `visible` off. */
17
22
  onDismiss: () => void
18
23
  /** The alert's heading. */
19
24
  title: string
@@ -8,6 +8,7 @@ import {Icon} from '../../atoms/icon'
8
8
  import {HostBoundary} from '../../host-context'
9
9
  import {splitForRipple} from '../../ripple-clip'
10
10
  import type {MenuAction} from '../menu'
11
+ import {menuActionColor} from '../menu/menu-parts'
11
12
  import type {ContextMenuComponent, ContextMenuExclusionProps, ContextMenuProps} from './context-menu.types'
12
13
 
13
14
  type Point = {x: number; y: number}
@@ -25,7 +26,7 @@ type MenuRowProps = {
25
26
  /** A row's `onClick` closes over its own action, so memoising it needs a component per row. */
26
27
  const MenuRow = ({action, onDismiss}: MenuRowProps) => {
27
28
  const theme = useAppTheme()
28
- const color = action.destructive ? 'content.destructive' : 'content.primary'
29
+ const color = menuActionColor(action)
29
30
 
30
31
  const onClick = useCallback(() => {
31
32
  onDismiss()
@@ -33,7 +34,7 @@ const MenuRow = ({action, onDismiss}: MenuRowProps) => {
33
34
  }, [action, onDismiss])
34
35
 
35
36
  return (
36
- <DropdownMenuItem onClick={onClick}>
37
+ <DropdownMenuItem enabled={!action.disabled} onClick={onClick}>
37
38
  <DropdownMenuItem.Text>
38
39
  <ComposeText color={theme.colorString(color)} style={theme.typography.body}>
39
40
  {action.label}
@@ -26,6 +26,7 @@ const ContextMenuRoot = ({
26
26
  () =>
27
27
  actions.map((action, index) => ({
28
28
  destructive: action.destructive,
29
+ disabled: action.disabled,
29
30
  id: String(index),
30
31
  image: action.icon ? iconResourceName(action.icon) : undefined,
31
32
  title: action.label,
@@ -43,6 +43,9 @@ const FabMenuRow = ({action, onSelect}: FabMenuRowProps) => {
43
43
  }
44
44
 
45
45
  /**
46
+ * `MenuAction.destructive` and `disabled` are not drawn: Material 3's `FloatingActionButtonMenuItem` takes neither a
47
+ * role nor an `enabled` state, and its pills keep the button's colours.
48
+ *
46
49
  * The menu fills its host and pins itself to the bottom end, so give it the corner of a screen — a `ZStack` over the
47
50
  * content — rather than a row of its own. The pills take the button's colours, so the menu reads as the button
48
51
  * unfolding rather than a second surface.
@@ -1,4 +1,5 @@
1
1
  import {Button, Image, Label, Menu} from '@expo/ui/swift-ui'
2
+ import {disabled} from '@expo/ui/swift-ui/modifiers'
2
3
  import {iconResourceName} from '@hzblj/expo-native-icons'
3
4
 
4
5
  import {Icon} from '../../atoms/icon'
@@ -10,6 +11,8 @@ import type {FabMenuComponent, FabMenuProps} from './fab-menu.types'
10
11
  /** Matches `Fab`, so a screen can switch between the two without the button changing shape. */
11
12
  const FAB_SIZE = 56
12
13
 
14
+ const DISABLED = [disabled(true)]
15
+
13
16
  export const FabMenu: FabMenuComponent = ({accessibilityLabel, actions, icon, testID}: FabMenuProps) => {
14
17
  useHostGuard('FabMenu')
15
18
 
@@ -35,7 +38,12 @@ export const FabMenu: FabMenuComponent = ({accessibilityLabel, actions, icon, te
35
38
  testID={testID}
36
39
  >
37
40
  {actions.map(action => (
38
- <Button key={action.label} onPress={action.onPress} role={action.destructive ? 'destructive' : 'default'}>
41
+ <Button
42
+ key={action.label}
43
+ modifiers={action.disabled ? DISABLED : undefined}
44
+ onPress={action.onPress}
45
+ role={action.destructive ? 'destructive' : 'default'}
46
+ >
39
47
  {/* A menu row is the system's Label: our icon is its image, from the asset catalog, and UIKit sizes it. */}
40
48
  <Label
41
49
  icon={action.icon ? <Image assetName={iconResourceName(action.icon)} /> : undefined}
@@ -0,0 +1,21 @@
1
+ import type {ColorToken} from '@hzblj/kara-theme'
2
+
3
+ import type {IconName} from '../../atoms/icon'
4
+ import type {MenuAction, MenuProps} from './menu.types'
5
+
6
+ /**
7
+ * An action's colour in a row kara draws itself (the Android menus and sheets): greyed when disabled, as `IconButton`
8
+ * greys, red when destructive. A Compose `DropdownMenuItem` cannot grey its own text here, since the text sets its
9
+ * colour explicitly.
10
+ */
11
+ export const menuActionColor = ({destructive, disabled}: MenuAction): ColorToken => {
12
+ if (disabled) {
13
+ return 'btn.content.disabled'
14
+ }
15
+
16
+ return destructive ? 'content.destructive' : 'content.primary'
17
+ }
18
+
19
+ /** A trigger with no text: the props' union guarantees its icon and the words a screen reader says for it. */
20
+ export const isIconOnly = (props: MenuProps): props is MenuProps & {accessibilityLabel: string; icon: IconName} =>
21
+ props.label === undefined
@@ -1,12 +1,14 @@
1
1
  import {Text as ComposeText, DropdownMenu, DropdownMenuItem} from '@expo/ui/jetpack-compose'
2
2
  import {useAppTheme} from '@hzblj/kara-theme'
3
- import {useCallback, useState} from 'react'
3
+ import {type ReactNode, useCallback, useState} from 'react'
4
4
  import {HStack} from '../../atoms/h-stack'
5
5
  import {Icon} from '../../atoms/icon'
6
+ import {IconButton} from '../../atoms/icon-button'
6
7
  import {Text} from '../../atoms/text'
7
8
  import {Glyph} from '../../glyph'
8
9
  import {useHostGuard} from '../../host-context'
9
10
  import type {MenuAction, MenuComponent, MenuProps} from './menu.types'
11
+ import {isIconOnly, menuActionColor} from './menu-parts'
10
12
 
11
13
  type MenuItemProps = {
12
14
  action: MenuAction
@@ -15,11 +17,12 @@ type MenuItemProps = {
15
17
 
16
18
  const MenuItem = ({action, onDismiss}: MenuItemProps) => {
17
19
  const theme = useAppTheme()
18
- const color = action.destructive ? 'content.destructive' : 'content.primary'
20
+ const color = menuActionColor(action)
19
21
 
20
22
  return (
21
23
  <DropdownMenuItem
22
24
  elementColors={{leadingIconColor: theme.color(color), textColor: theme.color(color)}}
25
+ enabled={!action.disabled}
23
26
  onClick={() => {
24
27
  onDismiss()
25
28
  action.onPress()
@@ -39,27 +42,53 @@ const MenuItem = ({action, onDismiss}: MenuItemProps) => {
39
42
  )
40
43
  }
41
44
 
45
+ const triggerOf = (props: MenuProps, open: () => void): ReactNode => {
46
+ if (isIconOnly(props)) {
47
+ // Material's overflow menu: an icon button, its label carried by the icon.
48
+ return (
49
+ <IconButton
50
+ accessibilityLabel={props.accessibilityLabel}
51
+ name={props.icon}
52
+ onPress={open}
53
+ testID={props.testID}
54
+ />
55
+ )
56
+ }
57
+
58
+ const {accessibilityLabel, icon, label, testID} = props
59
+
60
+ return (
61
+ <HStack accessibilityLabel={accessibilityLabel} align="center" onPress={open} spacing="s4" testID={testID}>
62
+ {icon ? (
63
+ <HStack align="center" spacing="s8">
64
+ <Icon color="content.accent" name={icon} size="s20" />
65
+ <Text color="content.accent">{label}</Text>
66
+ </HStack>
67
+ ) : (
68
+ <Text color="content.accent">{label}</Text>
69
+ )}
70
+ <Glyph color="content.accent" name="chevronDown" size="s16" />
71
+ </HStack>
72
+ )
73
+ }
74
+
42
75
  /**
43
76
  * Material's dropdown menu, anchored to its trigger. The items live in the `Items` slot, which Compose presents in
44
77
  * its own popup — anywhere else they would be laid out in place, on top of the trigger.
45
78
  */
46
- export const Menu: MenuComponent = ({actions, label}: MenuProps) => {
79
+ export const Menu: MenuComponent = (props: MenuProps) => {
47
80
  useHostGuard('Menu')
48
81
 
49
82
  const theme = useAppTheme()
50
83
  const [expanded, setExpanded] = useState(false)
84
+ const open = useCallback(() => setExpanded(true), [])
51
85
  const dismiss = useCallback(() => setExpanded(false), [])
52
86
 
53
87
  return (
54
88
  <DropdownMenu color={theme.color('surface.primary')} expanded={expanded} onDismissRequest={dismiss}>
55
- <DropdownMenu.Trigger>
56
- <HStack align="center" onPress={() => setExpanded(true)} spacing="s4">
57
- <Text color="content.accent">{label}</Text>
58
- <Glyph color="content.accent" name="chevronDown" size="s16" />
59
- </HStack>
60
- </DropdownMenu.Trigger>
89
+ <DropdownMenu.Trigger>{triggerOf(props, open)}</DropdownMenu.Trigger>
61
90
  <DropdownMenu.Items>
62
- {actions.map(action => (
91
+ {props.actions.map(action => (
63
92
  <MenuItem action={action} key={action.label} onDismiss={dismiss} />
64
93
  ))}
65
94
  </DropdownMenu.Items>
@@ -1,20 +1,70 @@
1
- import {Button, Menu as SwiftUIMenu} from '@expo/ui/swift-ui'
1
+ import {Button, Image, Label, Menu as SwiftUIMenu} from '@expo/ui/swift-ui'
2
+ import {contentShape, disabled, frame, resizable, shapes} from '@expo/ui/swift-ui/modifiers'
3
+ import {iconResourceName} from '@hzblj/expo-native-icons'
4
+ import {useAccessibilityModifiers} from '@hzblj/kara-sx'
5
+ import {spacing} from '@hzblj/kara-theme'
6
+ import type {ReactNode} from 'react'
2
7
 
8
+ import {Icon} from '../../atoms/icon'
9
+ import {ZStack} from '../../atoms/z-stack'
3
10
  import {useHostGuard} from '../../host-context'
4
11
  import type {MenuComponent, MenuProps} from './menu.types'
12
+ import {isIconOnly} from './menu-parts'
5
13
 
6
- export const Menu: MenuComponent = ({actions, label}: MenuProps) => {
14
+ const DISABLED = [disabled(true)]
15
+
16
+ /** Apple's least tap target, as `IconButton` takes it. */
17
+ const TARGET = 44
18
+
19
+ /** The whole square takes the tap, not only the glyph's own pixels. */
20
+ const WHOLE_TARGET = {modifiers: [contentShape(shapes.circle())]}
21
+
22
+ /**
23
+ * The icon beside the trigger's text, the size `Link` draws one beside body text. No colour of its own: the asset is a
24
+ * template image, so it takes the menu's tint with the text.
25
+ */
26
+ const LABEL_ICON = [resizable(), frame({height: spacing.s20, width: spacing.s20})]
27
+
28
+ const triggerOf = (props: MenuProps): ReactNode => {
29
+ if (isIconOnly(props)) {
30
+ // An icon alone is drawn as an `IconButton`: `content.primary`, its label carried by the icon.
31
+ return (
32
+ <ZStack h={TARGET} ios={WHOLE_TARGET} w={TARGET}>
33
+ <Icon accessibilityLabel={props.accessibilityLabel} color="content.primary" name={props.icon} />
34
+ </ZStack>
35
+ )
36
+ }
37
+
38
+ return props.icon ? (
39
+ <Label icon={<Image assetName={iconResourceName(props.icon)} modifiers={LABEL_ICON} />} title={props.label} />
40
+ ) : (
41
+ props.label
42
+ )
43
+ }
44
+
45
+ export const Menu: MenuComponent = (props: MenuProps) => {
7
46
  useHostGuard('Menu')
47
+ const {accessibilityLabel, actions, label, testID} = props
48
+ // An icon alone carries its own label; with text, `accessibilityLabel` replaces it on the menu.
49
+ const modifiers = useAccessibilityModifiers({
50
+ accessibilityLabel: label === undefined ? undefined : accessibilityLabel,
51
+ })
8
52
 
9
53
  return (
10
- <SwiftUIMenu label={label}>
54
+ <SwiftUIMenu label={triggerOf(props)} modifiers={modifiers} testID={testID}>
11
55
  {actions.map(action => (
12
56
  <Button
13
57
  key={action.label}
14
- label={action.label}
58
+ label={action.icon ? undefined : action.label}
59
+ modifiers={action.disabled ? DISABLED : undefined}
15
60
  onPress={action.onPress}
16
61
  role={action.destructive ? 'destructive' : 'default'}
17
- />
62
+ >
63
+ {/* A menu row is the system's Label: our icon is its image, from the asset catalog, and UIKit sizes it. */}
64
+ {action.icon ? (
65
+ <Label icon={<Image assetName={iconResourceName(action.icon)} />} title={action.label} />
66
+ ) : undefined}
67
+ </Button>
18
68
  ))}
19
69
  </SwiftUIMenu>
20
70
  )
@@ -10,8 +10,13 @@ export type MenuAction = {
10
10
  */
11
11
  destructive?: boolean
12
12
  /**
13
- * One of the app's icons, before the label. `ContextMenu` and `FabMenu` show it on both platforms, `Menu` and
14
- * `ActionSheet` on Android only.
13
+ * Greys the action out and stops it responding, for one that does not apply right now: "Paste" with nothing copied.
14
+ * Off by default. `FabMenu` ignores it on Android, where Material's FAB menu items have no disabled state.
15
+ */
16
+ disabled?: boolean
17
+ /**
18
+ * One of the app's icons, before the label. `Menu`, `ContextMenu` and `FabMenu` show it on both platforms,
19
+ * `ActionSheet` on Android only: an iOS confirmation dialog draws text alone.
15
20
  */
16
21
  icon?: IconName
17
22
  /** The action's text. Rows are keyed by it, so keep labels unique within one list. */
@@ -20,15 +25,33 @@ export type MenuAction = {
20
25
  onPress: () => void
21
26
  }
22
27
 
28
+ /** What the trigger needs: its text, or an icon and the words a screen reader says for it. */
29
+ type MenuTrigger = {label: string} | {accessibilityLabel: string; icon: IconName}
30
+
23
31
  /**
24
- * A text button that opens a list of actions: a SwiftUI `Menu` on iOS, a Material dropdown anchored to it on Android.
25
- * A native atom: it renders inside `<Screen>` or `<Island>`.
32
+ * A button that opens a list of actions: a SwiftUI `Menu` on iOS, a Material dropdown anchored to it on Android. The
33
+ * trigger is text, an icon and text, or an icon alone. A native atom: it renders inside `<Screen>` or `<Island>`.
26
34
  */
27
- export type MenuProps = {
35
+ export type MenuProps = MenuTrigger & {
36
+ /**
37
+ * What VoiceOver and TalkBack say for the trigger: "More options". Required with an `icon` and no `label`, since an
38
+ * icon has no words of its own. With a label, it replaces the label for screen readers.
39
+ */
40
+ accessibilityLabel?: string
28
41
  /** The menu's actions, top to bottom. */
29
42
  actions: MenuAction[]
30
- /** The trigger's text. Android draws it in the accent colour, with a chevron. */
31
- label: string
43
+ /**
44
+ * One of the app's icons on the trigger. Before the `label`, in the same colour, when there is one; alone, the
45
+ * trigger is drawn as an `IconButton` (`content.primary`, in a 44pt target on iOS and 48dp on Android) and needs an
46
+ * `accessibilityLabel`.
47
+ */
48
+ icon?: IconName
49
+ /**
50
+ * The trigger's text. Android draws it in the accent colour, with a chevron. Required unless there is an `icon`.
51
+ */
52
+ label?: string
53
+ /** Test identifier, forwarded to the trigger. Reach an action by its label once the menu is open. */
54
+ testID?: string
32
55
  }
33
56
 
34
57
  export type MenuComponent = (props: MenuProps) => ReactNode
@@ -32,6 +32,7 @@ const SheetRoot = ({
32
32
  onPresent,
33
33
  ref,
34
34
  scrollable = false,
35
+ systemBackground = false,
35
36
  testID,
36
37
  visible,
37
38
  }: SheetProps) => {
@@ -105,7 +106,7 @@ const SheetRoot = ({
105
106
  return (
106
107
  <View collapsable={false} pointerEvents="box-none" style={styles.host}>
107
108
  <TrueSheet
108
- backgroundColor={theme.color(bg)}
109
+ backgroundColor={systemBackground && Platform.OS === 'ios' ? undefined : theme.color(bg)}
109
110
  cornerRadius={CORNER_RADIUS}
110
111
  detents={detents}
111
112
  dimmed={dimmed}
@@ -51,7 +51,7 @@ export type SheetRef = {
51
51
  * one; native atoms in its content go in an `Island`.
52
52
  */
53
53
  export type SheetProps = {
54
- /** The sheet's colour. `surface.primary` by default. */
54
+ /** The sheet's colour. `surface.primary` by default; ignored on iOS with `systemBackground`. */
55
55
  bg?: ColorToken
56
56
  /** The content. A `<Sheet.Header>` among the direct children becomes the native header instead. */
57
57
  children: ReactNode
@@ -80,6 +80,12 @@ export type SheetProps = {
80
80
  * and a header scrolls with the iOS 26 edge effect. Off by default; set it with a scrolling body.
81
81
  */
82
82
  scrollable?: boolean
83
+ /**
84
+ * iOS: the system's own sheet background instead of `bg` — untinted Liquid Glass on iOS 26, a material blur before
85
+ * it — for a sheet that floats over content it should let through, like a map. Keep the content free of fills. Android
86
+ * keeps `bg`. Off by default.
87
+ */
88
+ systemBackground?: boolean
83
89
  /** Test identifier, forwarded to the native sheet. */
84
90
  testID?: string
85
91
  /** Presents the sheet while true; going false dismisses it. */