@hzblj/kara-components 0.1.0 → 0.2.1

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 (231) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/lib/module/atoms/badge/badge.android.js +8 -2
  3. package/lib/module/atoms/badge/badge.android.js.map +1 -1
  4. package/lib/module/atoms/button/button-tokens.js +39 -0
  5. package/lib/module/atoms/button/button-tokens.js.map +1 -1
  6. package/lib/module/atoms/button/button.android.js +40 -12
  7. package/lib/module/atoms/button/button.android.js.map +1 -1
  8. package/lib/module/atoms/button/button.ios.js +5 -2
  9. package/lib/module/atoms/button/button.ios.js.map +1 -1
  10. package/lib/module/atoms/checkbox/checkbox.android.js +17 -2
  11. package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
  12. package/lib/module/atoms/date-picker/date-picker.android.js +72 -0
  13. package/lib/module/atoms/date-picker/date-picker.android.js.map +1 -0
  14. package/lib/module/atoms/date-picker/{date-picker.js → date-picker.ios.js} +4 -7
  15. package/lib/module/atoms/date-picker/date-picker.ios.js.map +1 -0
  16. package/lib/module/atoms/date-picker/index.js +1 -1
  17. package/lib/module/atoms/date-picker/index.js.map +1 -1
  18. package/lib/module/atoms/fab/fab.android.js +5 -3
  19. package/lib/module/atoms/fab/fab.android.js.map +1 -1
  20. package/lib/module/atoms/glass-view/glass-fallback.js +5 -2
  21. package/lib/module/atoms/glass-view/glass-fallback.js.map +1 -1
  22. package/lib/module/atoms/icon/icon.android.js +10 -12
  23. package/lib/module/atoms/icon/icon.android.js.map +1 -1
  24. package/lib/module/atoms/icon/icon.ios.js +15 -3
  25. package/lib/module/atoms/icon/icon.ios.js.map +1 -1
  26. package/lib/module/atoms/icon/index.js +0 -1
  27. package/lib/module/atoms/icon/index.js.map +1 -1
  28. package/lib/module/atoms/index.js +1 -1
  29. package/lib/module/atoms/index.js.map +1 -1
  30. package/lib/module/atoms/list/list.android.js +8 -2
  31. package/lib/module/atoms/list/list.android.js.map +1 -1
  32. package/lib/module/atoms/picker/index.js +1 -1
  33. package/lib/module/atoms/picker/index.js.map +1 -1
  34. package/lib/module/atoms/picker/picker.android.js +87 -0
  35. package/lib/module/atoms/picker/picker.android.js.map +1 -0
  36. package/lib/module/atoms/picker/{picker.js → picker.ios.js} +1 -1
  37. package/lib/module/atoms/picker/picker.ios.js.map +1 -0
  38. package/lib/module/atoms/rn-icon/index.js +5 -0
  39. package/lib/module/atoms/rn-icon/index.js.map +1 -0
  40. package/lib/module/atoms/rn-icon/rn-icon.android.js +29 -0
  41. package/lib/module/atoms/rn-icon/rn-icon.android.js.map +1 -0
  42. package/lib/module/atoms/rn-icon/rn-icon.ios.js +29 -0
  43. package/lib/module/atoms/rn-icon/rn-icon.ios.js.map +1 -0
  44. package/lib/module/atoms/rn-icon/rn-icon.types.js +4 -0
  45. package/lib/module/atoms/rn-icon/rn-icon.types.js.map +1 -0
  46. package/lib/module/atoms/surface/surface.android.js +33 -5
  47. package/lib/module/atoms/surface/surface.android.js.map +1 -1
  48. package/lib/module/atoms/switch/switch.android.js +14 -1
  49. package/lib/module/atoms/switch/switch.android.js.map +1 -1
  50. package/lib/module/glyph/glyph-button.android.js +25 -0
  51. package/lib/module/glyph/glyph-button.android.js.map +1 -0
  52. package/lib/module/glyph/glyph-button.ios.js +33 -0
  53. package/lib/module/glyph/glyph-button.ios.js.map +1 -0
  54. package/lib/module/glyph/glyph-button.types.js +4 -0
  55. package/lib/module/glyph/glyph-button.types.js.map +1 -0
  56. package/lib/module/glyph/glyph.android.js +20 -0
  57. package/lib/module/glyph/glyph.android.js.map +1 -0
  58. package/lib/module/glyph/glyph.ios.js +24 -0
  59. package/lib/module/glyph/glyph.ios.js.map +1 -0
  60. package/lib/module/glyph/glyph.types.js +4 -0
  61. package/lib/module/glyph/glyph.types.js.map +1 -0
  62. package/lib/module/glyph/index.js +8 -0
  63. package/lib/module/glyph/index.js.map +1 -0
  64. package/lib/module/glyph/rn-glyph.android.js +25 -0
  65. package/lib/module/glyph/rn-glyph.android.js.map +1 -0
  66. package/lib/module/glyph/rn-glyph.ios.js +26 -0
  67. package/lib/module/glyph/rn-glyph.ios.js.map +1 -0
  68. package/lib/module/molecules/collapsible/collapsible.android.js +2 -2
  69. package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
  70. package/lib/module/molecules/labeled-content/labeled-content.js +2 -1
  71. package/lib/module/molecules/labeled-content/labeled-content.js.map +1 -1
  72. package/lib/module/molecules/list-item/list-item.js +2 -1
  73. package/lib/module/molecules/list-item/list-item.js.map +1 -1
  74. package/lib/module/molecules/navigation-bar/navigation-bar.android.js +8 -4
  75. package/lib/module/molecules/navigation-bar/navigation-bar.android.js.map +1 -1
  76. package/lib/module/molecules/radio-group/radio-group.android.js +5 -0
  77. package/lib/module/molecules/radio-group/radio-group.android.js.map +1 -1
  78. package/lib/module/molecules/radio-group/radio-group.ios.js +2 -2
  79. package/lib/module/molecules/radio-group/radio-group.ios.js.map +1 -1
  80. package/lib/module/molecules/search-bar/search-bar.js +10 -23
  81. package/lib/module/molecules/search-bar/search-bar.js.map +1 -1
  82. package/lib/module/molecules/stepper/stepper.android.js +7 -5
  83. package/lib/module/molecules/stepper/stepper.android.js.map +1 -1
  84. package/lib/module/organisms/alert/alert.android.js +42 -13
  85. package/lib/module/organisms/alert/alert.android.js.map +1 -1
  86. package/lib/module/organisms/context-menu/context-menu.android.js +82 -31
  87. package/lib/module/organisms/context-menu/context-menu.android.js.map +1 -1
  88. package/lib/module/organisms/context-menu/context-menu.ios.js +37 -16
  89. package/lib/module/organisms/context-menu/context-menu.ios.js.map +1 -1
  90. package/lib/module/organisms/fab-menu/fab-menu.android.js +6 -2
  91. package/lib/module/organisms/fab-menu/fab-menu.android.js.map +1 -1
  92. package/lib/module/organisms/fab-menu/fab-menu.ios.js +15 -6
  93. package/lib/module/organisms/fab-menu/fab-menu.ios.js.map +1 -1
  94. package/lib/module/organisms/form/form.android.js +1 -0
  95. package/lib/module/organisms/form/form.android.js.map +1 -1
  96. package/lib/module/organisms/index.js +0 -1
  97. package/lib/module/organisms/index.js.map +1 -1
  98. package/lib/module/organisms/menu/menu.android.js +51 -33
  99. package/lib/module/organisms/menu/menu.android.js.map +1 -1
  100. package/lib/typescript/atoms/button/button-tokens.d.ts +6 -1
  101. package/lib/typescript/atoms/date-picker/date-picker.d.ts +1 -2
  102. package/lib/typescript/atoms/date-picker/date-picker.ios.d.ts +3 -0
  103. package/lib/typescript/atoms/glass-view/glass-fallback.d.ts +4 -1
  104. package/lib/typescript/atoms/glass-view/glass-view.types.d.ts +1 -1
  105. package/lib/typescript/atoms/icon/icon.ios.d.ts +4 -0
  106. package/lib/typescript/atoms/icon/icon.types.d.ts +18 -1
  107. package/lib/typescript/atoms/icon/index.d.ts +0 -1
  108. package/lib/typescript/atoms/index.d.ts +1 -1
  109. package/lib/typescript/atoms/list/list.types.d.ts +9 -0
  110. package/lib/typescript/atoms/picker/picker.d.ts +1 -2
  111. package/lib/typescript/atoms/picker/picker.ios.d.ts +2 -0
  112. package/lib/typescript/atoms/rn-icon/index.d.ts +2 -0
  113. package/lib/typescript/atoms/rn-icon/rn-icon.d.ts +1 -0
  114. package/lib/typescript/atoms/rn-icon/rn-icon.ios.d.ts +3 -0
  115. package/lib/typescript/atoms/rn-icon/rn-icon.types.d.ts +5 -0
  116. package/lib/typescript/glyph/glyph-button.d.ts +1 -0
  117. package/lib/typescript/glyph/glyph-button.ios.d.ts +2 -0
  118. package/lib/typescript/glyph/glyph-button.types.d.ts +10 -0
  119. package/lib/typescript/glyph/glyph.d.ts +1 -0
  120. package/lib/typescript/glyph/glyph.ios.d.ts +3 -0
  121. package/lib/typescript/glyph/glyph.types.d.ts +13 -0
  122. package/lib/typescript/glyph/index.d.ts +5 -0
  123. package/lib/typescript/glyph/rn-glyph.d.ts +1 -0
  124. package/lib/typescript/glyph/rn-glyph.ios.d.ts +3 -0
  125. package/lib/typescript/molecules/scroll-edge-container/scroll-edge-container.types.d.ts +3 -0
  126. package/lib/typescript/organisms/context-menu/context-menu.types.d.ts +28 -2
  127. package/lib/typescript/organisms/fab-menu/fab-menu.types.d.ts +1 -1
  128. package/lib/typescript/organisms/form/form.types.d.ts +5 -0
  129. package/lib/typescript/organisms/index.d.ts +0 -1
  130. package/package.json +4 -6
  131. package/src/atoms/badge/badge.android.tsx +5 -2
  132. package/src/atoms/button/button-tokens.ts +49 -0
  133. package/src/atoms/button/button.android.tsx +46 -12
  134. package/src/atoms/button/button.ios.tsx +5 -2
  135. package/src/atoms/checkbox/checkbox.android.tsx +21 -4
  136. package/src/atoms/date-picker/date-picker.android.tsx +74 -0
  137. package/src/atoms/date-picker/date-picker.ios.tsx +27 -0
  138. package/src/atoms/fab/fab.android.tsx +4 -1
  139. package/src/atoms/glass-view/glass-fallback.tsx +5 -2
  140. package/src/atoms/glass-view/glass-view.types.ts +1 -1
  141. package/src/atoms/icon/icon.android.tsx +11 -7
  142. package/src/atoms/icon/icon.ios.tsx +25 -5
  143. package/src/atoms/icon/icon.types.ts +18 -24
  144. package/src/atoms/icon/index.ts +0 -1
  145. package/src/atoms/index.ts +1 -1
  146. package/src/atoms/list/list.android.tsx +12 -1
  147. package/src/atoms/list/list.types.ts +9 -0
  148. package/src/atoms/picker/picker.android.tsx +88 -0
  149. package/src/atoms/rn-icon/index.ts +2 -0
  150. package/src/atoms/rn-icon/rn-icon.android.tsx +28 -0
  151. package/src/atoms/rn-icon/rn-icon.ios.tsx +28 -0
  152. package/src/atoms/rn-icon/rn-icon.types.ts +8 -0
  153. package/src/atoms/surface/surface.android.tsx +28 -4
  154. package/src/atoms/switch/switch.android.tsx +24 -3
  155. package/src/glyph/glyph-button.android.tsx +25 -0
  156. package/src/glyph/glyph-button.ios.tsx +37 -0
  157. package/src/glyph/glyph-button.types.ts +12 -0
  158. package/src/glyph/glyph.android.tsx +12 -0
  159. package/src/glyph/glyph.ios.tsx +15 -0
  160. package/src/glyph/glyph.types.ts +15 -0
  161. package/src/glyph/index.ts +5 -0
  162. package/src/glyph/rn-glyph.android.tsx +14 -0
  163. package/src/glyph/rn-glyph.ios.tsx +16 -0
  164. package/src/molecules/collapsible/collapsible.android.tsx +2 -2
  165. package/src/molecules/labeled-content/labeled-content.tsx +2 -1
  166. package/src/molecules/list-item/list-item.tsx +2 -1
  167. package/src/molecules/navigation-bar/navigation-bar.android.tsx +11 -4
  168. package/src/molecules/radio-group/radio-group.android.tsx +11 -2
  169. package/src/molecules/radio-group/radio-group.ios.tsx +2 -2
  170. package/src/molecules/scroll-edge-container/scroll-edge-container.types.ts +3 -0
  171. package/src/molecules/search-bar/search-bar.tsx +4 -9
  172. package/src/molecules/stepper/stepper.android.tsx +3 -3
  173. package/src/organisms/alert/alert.android.tsx +44 -7
  174. package/src/organisms/context-menu/context-menu.android.tsx +71 -37
  175. package/src/organisms/context-menu/context-menu.ios.tsx +40 -18
  176. package/src/organisms/context-menu/context-menu.types.ts +29 -3
  177. package/src/organisms/fab-menu/fab-menu.android.tsx +3 -2
  178. package/src/organisms/fab-menu/fab-menu.ios.tsx +13 -12
  179. package/src/organisms/fab-menu/fab-menu.types.ts +1 -1
  180. package/src/organisms/form/form.android.tsx +1 -1
  181. package/src/organisms/form/form.types.ts +5 -0
  182. package/src/organisms/index.ts +0 -1
  183. package/src/organisms/menu/menu.android.tsx +36 -30
  184. package/lib/module/atoms/countdown/countdown-frame.js +0 -115
  185. package/lib/module/atoms/countdown/countdown-frame.js.map +0 -1
  186. package/lib/module/atoms/countdown/countdown-units.js +0 -25
  187. package/lib/module/atoms/countdown/countdown-units.js.map +0 -1
  188. package/lib/module/atoms/countdown/countdown.android.js +0 -53
  189. package/lib/module/atoms/countdown/countdown.android.js.map +0 -1
  190. package/lib/module/atoms/countdown/countdown.ios.js +0 -50
  191. package/lib/module/atoms/countdown/countdown.ios.js.map +0 -1
  192. package/lib/module/atoms/countdown/countdown.types.js +0 -4
  193. package/lib/module/atoms/countdown/countdown.types.js.map +0 -1
  194. package/lib/module/atoms/countdown/index.js +0 -5
  195. package/lib/module/atoms/countdown/index.js.map +0 -1
  196. package/lib/module/atoms/date-picker/date-picker.js.map +0 -1
  197. package/lib/module/atoms/icon/icon-names.js +0 -53
  198. package/lib/module/atoms/icon/icon-names.js.map +0 -1
  199. package/lib/module/atoms/picker/picker.js.map +0 -1
  200. package/lib/module/organisms/rn-context-menu/index.js +0 -5
  201. package/lib/module/organisms/rn-context-menu/index.js.map +0 -1
  202. package/lib/module/organisms/rn-context-menu/rn-context-menu.android.js +0 -101
  203. package/lib/module/organisms/rn-context-menu/rn-context-menu.android.js.map +0 -1
  204. package/lib/module/organisms/rn-context-menu/rn-context-menu.ios.js +0 -44
  205. package/lib/module/organisms/rn-context-menu/rn-context-menu.ios.js.map +0 -1
  206. package/lib/module/organisms/rn-context-menu/rn-context-menu.types.js +0 -4
  207. package/lib/module/organisms/rn-context-menu/rn-context-menu.types.js.map +0 -1
  208. package/lib/typescript/atoms/countdown/countdown-frame.d.ts +0 -17
  209. package/lib/typescript/atoms/countdown/countdown-units.d.ts +0 -5
  210. package/lib/typescript/atoms/countdown/countdown.d.ts +0 -1
  211. package/lib/typescript/atoms/countdown/countdown.ios.d.ts +0 -2
  212. package/lib/typescript/atoms/countdown/countdown.types.d.ts +0 -21
  213. package/lib/typescript/atoms/countdown/index.d.ts +0 -2
  214. package/lib/typescript/atoms/icon/icon-names.d.ts +0 -50
  215. package/lib/typescript/organisms/rn-context-menu/index.d.ts +0 -2
  216. package/lib/typescript/organisms/rn-context-menu/rn-context-menu.d.ts +0 -1
  217. package/lib/typescript/organisms/rn-context-menu/rn-context-menu.ios.d.ts +0 -2
  218. package/lib/typescript/organisms/rn-context-menu/rn-context-menu.types.d.ts +0 -33
  219. package/src/atoms/countdown/countdown-frame.tsx +0 -123
  220. package/src/atoms/countdown/countdown-units.ts +0 -37
  221. package/src/atoms/countdown/countdown.android.tsx +0 -49
  222. package/src/atoms/countdown/countdown.ios.tsx +0 -46
  223. package/src/atoms/countdown/countdown.types.ts +0 -24
  224. package/src/atoms/countdown/index.ts +0 -2
  225. package/src/atoms/date-picker/date-picker.tsx +0 -26
  226. package/src/atoms/icon/icon-names.ts +0 -53
  227. package/src/organisms/rn-context-menu/index.ts +0 -2
  228. package/src/organisms/rn-context-menu/rn-context-menu.android.tsx +0 -90
  229. package/src/organisms/rn-context-menu/rn-context-menu.ios.tsx +0 -47
  230. package/src/organisms/rn-context-menu/rn-context-menu.types.ts +0 -36
  231. /package/src/atoms/picker/{picker.tsx → picker.ios.tsx} +0 -0
@@ -0,0 +1,88 @@
1
+ import {
2
+ Text as ComposeText,
3
+ DropdownMenuItem,
4
+ ExposedDropdownMenu,
5
+ ExposedDropdownMenuBox,
6
+ Host,
7
+ OutlinedTextField,
8
+ useNativeState,
9
+ } from '@expo/ui/jetpack-compose'
10
+ import {fillMaxWidth, menuAnchor, testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
11
+ import {useAppTheme} from '@hzblj/kara-theme'
12
+ import {useEffect, useMemo, useState} from 'react'
13
+
14
+ import {Glyph} from '../../glyph'
15
+ import {HostBoundary} from '../../host-context'
16
+ import type {PickerComponent, PickerProps} from './picker.types'
17
+
18
+ const MATCH_HEIGHT_ONLY = {horizontal: false, vertical: true}
19
+
20
+ /**
21
+ * Material's exposed dropdown: a read-only outlined field the width of its container, opening a menu of the options
22
+ * under it. Its own host, themed like an `Island`, so it drops into React Native layout like the iOS picker does.
23
+ */
24
+ export const Picker: PickerComponent = ({disabled = false, onValueChange, options, testID, value}: PickerProps) => {
25
+ const theme = useAppTheme()
26
+ const [expanded, setExpanded] = useState(false)
27
+ const selectedLabel = options.find(option => option.value === value)?.label ?? ''
28
+ const label = useNativeState(selectedLabel)
29
+ const fieldModifiers = useMemo(() => {
30
+ const list = [fillMaxWidth(), menuAnchor('primaryNotEditable', !disabled)]
31
+
32
+ return testID ? [...list, testIdModifier(testID)] : list
33
+ }, [disabled, testID])
34
+
35
+ // `useNativeState` takes only its initial value; a new selection is pushed to the field.
36
+ useEffect(() => {
37
+ label.set(selectedLabel)
38
+ }, [label, selectedLabel])
39
+
40
+ return (
41
+ <Host colorScheme={theme.dark ? 'dark' : 'light'} matchContents={MATCH_HEIGHT_ONLY} seedColor={theme.materialSeed}>
42
+ <HostBoundary>
43
+ <ExposedDropdownMenuBox expanded={expanded} onExpandedChange={disabled ? undefined : setExpanded}>
44
+ <OutlinedTextField
45
+ enabled={!disabled}
46
+ modifiers={fieldModifiers}
47
+ readOnly
48
+ singleLine
49
+ textStyle={{
50
+ color: theme.colorString('content.primary'),
51
+ fontFamily: theme.typography.body.fontFamily,
52
+ fontWeight: theme.typography.body.fontWeight,
53
+ }}
54
+ value={label}
55
+ >
56
+ <OutlinedTextField.TrailingIcon>
57
+ <Glyph color="content.secondary" name="chevronDown" size="s20" />
58
+ </OutlinedTextField.TrailingIcon>
59
+ </OutlinedTextField>
60
+ <ExposedDropdownMenu
61
+ containerColor={theme.color('surface.primary')}
62
+ expanded={expanded}
63
+ onDismissRequest={() => setExpanded(false)}
64
+ >
65
+ {options.map(option => (
66
+ <DropdownMenuItem
67
+ key={option.value}
68
+ onClick={() => {
69
+ setExpanded(false)
70
+ onValueChange?.(option.value)
71
+ }}
72
+ >
73
+ <DropdownMenuItem.Text>
74
+ <ComposeText
75
+ color={theme.colorString(option.value === value ? 'content.accent' : 'content.primary')}
76
+ style={theme.typography.body}
77
+ >
78
+ {option.label}
79
+ </ComposeText>
80
+ </DropdownMenuItem.Text>
81
+ </DropdownMenuItem>
82
+ ))}
83
+ </ExposedDropdownMenu>
84
+ </ExposedDropdownMenuBox>
85
+ </HostBoundary>
86
+ </Host>
87
+ )
88
+ }
@@ -0,0 +1,2 @@
1
+ export * from './rn-icon'
2
+ export * from './rn-icon.types'
@@ -0,0 +1,28 @@
1
+ import {iconResourceName} from '@hzblj/kara-native'
2
+ import {IconView} from '@hzblj/kara-native/jetpack-compose'
3
+ import {spacing, useAppTheme} from '@hzblj/kara-theme'
4
+ import {useMemo} from 'react'
5
+
6
+ import type {RnIconComponent, RnIconProps} from './rn-icon.types'
7
+
8
+ /** An `ImageView` over the app's vector drawable — what a list row wants, rather than a Compose host per row. */
9
+ export const RnIcon: RnIconComponent = ({
10
+ accessibilityLabel,
11
+ color = 'content.primary',
12
+ name,
13
+ size = 's24',
14
+ testID,
15
+ }: RnIconProps) => {
16
+ const theme = useAppTheme()
17
+ const style = useMemo(() => ({height: spacing[size], width: spacing[size]}), [size])
18
+
19
+ return (
20
+ <IconView
21
+ accessibilityLabel={accessibilityLabel}
22
+ iconColor={theme.color(color)}
23
+ resourceName={iconResourceName(name)}
24
+ style={style}
25
+ testID={testID}
26
+ />
27
+ )
28
+ }
@@ -0,0 +1,28 @@
1
+ import {iconResourceName} from '@hzblj/kara-native'
2
+ import {IconView} from '@hzblj/kara-native/swift-ui'
3
+ import {spacing, useAppTheme} from '@hzblj/kara-theme'
4
+ import {useMemo} from 'react'
5
+
6
+ import type {RnIconComponent, RnIconProps} from './rn-icon.types'
7
+
8
+ /** A `UIImageView` over the app's asset-catalog icon — what a list row wants, rather than a SwiftUI host per row. */
9
+ export const RnIcon: RnIconComponent = ({
10
+ accessibilityLabel,
11
+ color = 'content.primary',
12
+ name,
13
+ size = 's24',
14
+ testID,
15
+ }: RnIconProps) => {
16
+ const theme = useAppTheme()
17
+ const style = useMemo(() => ({height: spacing[size], width: spacing[size]}), [size])
18
+
19
+ return (
20
+ <IconView
21
+ accessibilityLabel={accessibilityLabel}
22
+ assetName={iconResourceName(name)}
23
+ iconColor={theme.color(color)}
24
+ style={style}
25
+ testID={testID}
26
+ />
27
+ )
28
+ }
@@ -0,0 +1,8 @@
1
+ import type {ReactNode} from 'react'
2
+
3
+ import type {IconProps} from '../icon'
4
+
5
+ /** The same contract as `Icon`, for the React Native tier — anywhere, no `Screen` or `Island` needed. */
6
+ export type RnIconProps = IconProps
7
+
8
+ export type RnIconComponent = (props: RnIconProps) => ReactNode
@@ -1,16 +1,40 @@
1
- import {Surface as ComposeSurface, Shape} from '@expo/ui/jetpack-compose'
1
+ import {Column, Surface as ComposeSurface, Shape} from '@expo/ui/jetpack-compose'
2
+ import {testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
2
3
  import {useSxModifiers} from '@hzblj/kara-sx'
3
4
  import {radii, useAppTheme} from '@hzblj/kara-theme'
5
+ import {useMemo} from 'react'
4
6
  import {useHostGuard} from '../../host-context'
5
7
  import type {SurfaceComponent, SurfaceProps} from './surface.types'
6
8
 
7
9
  const cornerRadii = (radius: number) => ({bottomEnd: radius, bottomStart: radius, topEnd: radius, topStart: radius})
8
10
 
9
- export const Surface: SurfaceComponent = ({children, glass = false, testID, ...sx}: SurfaceProps) => {
11
+ /**
12
+ * Material's `Surface`, with the padding on a `Column` inside it. Compose applies a `Surface`'s own modifiers before it
13
+ * draws the shape, so padding there would inset the background instead of the content — and a bare `Surface` stacks
14
+ * its children on top of each other rather than in a column.
15
+ */
16
+ export const Surface: SurfaceComponent = ({
17
+ children,
18
+ glass = false,
19
+ p = 's16',
20
+ pb,
21
+ pl,
22
+ pr,
23
+ pt,
24
+ px,
25
+ py,
26
+ testID,
27
+ ...sx
28
+ }: SurfaceProps) => {
10
29
  useHostGuard('Surface')
11
30
 
12
31
  const theme = useAppTheme()
13
- const modifiers = useSxModifiers({...sx, bg: undefined, p: sx.p ?? 's16'})
32
+ const sxModifiers = useSxModifiers({...sx, bg: undefined})
33
+ const modifiers = useMemo(
34
+ () => (testID ? [...sxModifiers, testIdModifier(testID)] : sxModifiers),
35
+ [sxModifiers, testID]
36
+ )
37
+ const contentModifiers = useSxModifiers({p, pb, pl, pr, pt, px, py, wide: sx.wide})
14
38
 
15
39
  return (
16
40
  <ComposeSurface
@@ -20,7 +44,7 @@ export const Surface: SurfaceComponent = ({children, glass = false, testID, ...s
20
44
  shape={Shape.RoundedCorner({cornerRadii: cornerRadii(radii[sx.borderRadius ?? 'large'])})}
21
45
  tonalElevation={glass ? 3 : 0}
22
46
  >
23
- {children}
47
+ <Column modifiers={contentModifiers}>{children}</Column>
24
48
  </ComposeSurface>
25
49
  )
26
50
  }
@@ -1,21 +1,42 @@
1
1
  import {Switch as ComposeSwitch, Row, Text} from '@expo/ui/jetpack-compose'
2
+ import {clickable, fillMaxWidth} from '@expo/ui/jetpack-compose/modifiers'
2
3
  import {useAppTheme} from '@hzblj/kara-theme'
4
+ import {useMemo} from 'react'
3
5
 
4
6
  import {useHostGuard} from '../../host-context'
5
7
  import type {SwitchComponent, SwitchProps} from './switch.types'
6
8
 
9
+ /**
10
+ * Material's switch, tinted with the accent like SwiftUI's toggle. With a label it is a full-width row — label at the
11
+ * start, switch at the end — and the whole row toggles, the way a Material settings row does.
12
+ */
7
13
  export const Switch: SwitchComponent = ({disabled = false, label, onValueChange, value}: SwitchProps) => {
8
14
  useHostGuard('Switch')
9
15
  const theme = useAppTheme()
10
- const control = <ComposeSwitch enabled={!disabled} onCheckedChange={onValueChange} value={value} />
16
+ const rowModifiers = useMemo(
17
+ () => (disabled || !onValueChange ? [fillMaxWidth()] : [fillMaxWidth(), clickable(() => onValueChange(!value))]),
18
+ [disabled, onValueChange, value]
19
+ )
20
+ const control = (
21
+ <ComposeSwitch
22
+ colors={{
23
+ checkedBorderColor: theme.color('fill.accent.primary'),
24
+ checkedThumbColor: theme.color('content.on.bg'),
25
+ checkedTrackColor: theme.color('fill.accent.primary'),
26
+ }}
27
+ enabled={!disabled}
28
+ onCheckedChange={onValueChange}
29
+ value={value}
30
+ />
31
+ )
11
32
 
12
33
  if (!label) {
13
34
  return control
14
35
  }
15
36
 
16
37
  return (
17
- <Row horizontalArrangement="spaceBetween" verticalAlignment="center">
18
- <Text color={theme.colorString('content.primary')} style={theme.typography.body}>
38
+ <Row horizontalArrangement="spaceBetween" modifiers={rowModifiers} verticalAlignment="center">
39
+ <Text color={theme.colorString(disabled ? 'content.tertiary' : 'content.primary')} style={theme.typography.body}>
19
40
  {label}
20
41
  </Text>
21
42
  {control}
@@ -0,0 +1,25 @@
1
+ import {IconButton as ComposeIconButton} from '@expo/ui/jetpack-compose'
2
+ import {GLYPHS} from '@hzblj/kara-native'
3
+ import {DrawableIcon} from '@hzblj/kara-native/jetpack-compose'
4
+ import {useAppTheme} from '@hzblj/kara-theme'
5
+
6
+ import type {GlyphButtonComponent, GlyphButtonProps} from './glyph-button.types'
7
+
8
+ export const GlyphButton: GlyphButtonComponent = ({
9
+ accessibilityLabel,
10
+ disabled = false,
11
+ name,
12
+ onPress,
13
+ }: GlyphButtonProps) => {
14
+ const theme = useAppTheme()
15
+
16
+ return (
17
+ <ComposeIconButton enabled={!disabled} onClick={onPress}>
18
+ <DrawableIcon
19
+ accessibilityLabel={accessibilityLabel}
20
+ iconColor={theme.color(disabled ? 'btn.content.disabled' : 'content.primary')}
21
+ resourceName={GLYPHS[name].android}
22
+ />
23
+ </ComposeIconButton>
24
+ )
25
+ }
@@ -0,0 +1,37 @@
1
+ import {Button} from '@expo/ui/swift-ui'
2
+ import {
3
+ buttonStyle,
4
+ disabled as disabledModifier,
5
+ accessibilityLabel as labelModifier,
6
+ } from '@expo/ui/swift-ui/modifiers'
7
+ import {useMemo} from 'react'
8
+
9
+ import {Glyph} from './glyph'
10
+ import type {GlyphButtonComponent, GlyphButtonProps} from './glyph-button.types'
11
+
12
+ export const GlyphButton: GlyphButtonComponent = ({
13
+ accessibilityLabel,
14
+ disabled = false,
15
+ name,
16
+ onPress,
17
+ }: GlyphButtonProps) => {
18
+ const modifiers = useMemo(() => {
19
+ const list = [buttonStyle('plain')]
20
+
21
+ if (disabled) {
22
+ list.push(disabledModifier(true))
23
+ }
24
+
25
+ if (accessibilityLabel) {
26
+ list.push(labelModifier(accessibilityLabel))
27
+ }
28
+
29
+ return list
30
+ }, [accessibilityLabel, disabled])
31
+
32
+ return (
33
+ <Button modifiers={modifiers} onPress={onPress}>
34
+ <Glyph color={disabled ? 'btn.content.disabled' : 'content.primary'} name={name} />
35
+ </Button>
36
+ )
37
+ }
@@ -0,0 +1,12 @@
1
+ import type {GlyphName} from '@hzblj/kara-native'
2
+ import type {ReactNode} from 'react'
3
+
4
+ /** An icon button over one of kara's glyphs — for the controls its own components build (a stepper, a bar). */
5
+ export type GlyphButtonProps = {
6
+ accessibilityLabel?: string
7
+ disabled?: boolean
8
+ name: GlyphName
9
+ onPress?: () => void
10
+ }
11
+
12
+ export type GlyphButtonComponent = (props: GlyphButtonProps) => ReactNode
@@ -0,0 +1,12 @@
1
+ import {GLYPHS} from '@hzblj/kara-native'
2
+ import {DrawableIcon} from '@hzblj/kara-native/jetpack-compose'
3
+ import {spacing, useAppTheme} from '@hzblj/kara-theme'
4
+
5
+ import type {GlyphComponent, GlyphProps} from './glyph.types'
6
+
7
+ /** The Material Symbol `@hzblj/kara-native` ships as a drawable — inside a Compose host. */
8
+ export const Glyph: GlyphComponent = ({color = 'content.primary', name, size = 's24'}: GlyphProps) => {
9
+ const theme = useAppTheme()
10
+
11
+ return <DrawableIcon iconColor={theme.color(color)} iconSize={spacing[size]} resourceName={GLYPHS[name].android} />
12
+ }
@@ -0,0 +1,15 @@
1
+ import {Image} from '@expo/ui/swift-ui'
2
+ import {accessibilityHidden} from '@expo/ui/swift-ui/modifiers'
3
+ import {GLYPHS} from '@hzblj/kara-native'
4
+ import {spacing, useAppTheme} from '@hzblj/kara-theme'
5
+
6
+ import type {GlyphComponent, GlyphProps} from './glyph.types'
7
+
8
+ const DECORATIVE = [accessibilityHidden(true)]
9
+
10
+ /** The SF Symbol the system already has — inside a SwiftUI host. */
11
+ export const Glyph: GlyphComponent = ({color = 'content.primary', name, size = 's24'}: GlyphProps) => {
12
+ const theme = useAppTheme()
13
+
14
+ return <Image color={theme.color(color)} modifiers={DECORATIVE} size={spacing[size]} systemName={GLYPHS[name].ios} />
15
+ }
@@ -0,0 +1,15 @@
1
+ import type {GlyphName} from '@hzblj/kara-native'
2
+ import type {ColorToken, SpaceSize} from '@hzblj/kara-theme'
3
+ import type {ReactNode} from 'react'
4
+
5
+ /**
6
+ * One of the few glyphs kara's own components draw — a search field's magnifier, a row's chevron. Internal: apps draw
7
+ * their own icons with `Icon` / `RnIcon`.
8
+ */
9
+ export type GlyphProps = {
10
+ color?: ColorToken
11
+ name: GlyphName
12
+ size?: SpaceSize
13
+ }
14
+
15
+ export type GlyphComponent = (props: GlyphProps) => ReactNode
@@ -0,0 +1,5 @@
1
+ export * from './glyph'
2
+ export * from './glyph.types'
3
+ export * from './glyph-button'
4
+ export * from './glyph-button.types'
5
+ export * from './rn-glyph'
@@ -0,0 +1,14 @@
1
+ import {GLYPHS} from '@hzblj/kara-native'
2
+ import {IconView} from '@hzblj/kara-native/jetpack-compose'
3
+ import {spacing, useAppTheme} from '@hzblj/kara-theme'
4
+ import {useMemo} from 'react'
5
+
6
+ import type {GlyphComponent, GlyphProps} from './glyph.types'
7
+
8
+ /** The Material Symbol drawable in an `ImageView` — the React Native tier. */
9
+ export const RnGlyph: GlyphComponent = ({color = 'content.primary', name, size = 's24'}: GlyphProps) => {
10
+ const theme = useAppTheme()
11
+ const style = useMemo(() => ({height: spacing[size], width: spacing[size]}), [size])
12
+
13
+ return <IconView iconColor={theme.color(color)} resourceName={GLYPHS[name].android} style={style} />
14
+ }
@@ -0,0 +1,16 @@
1
+ import {GLYPHS} from '@hzblj/kara-native'
2
+ import {IconView} from '@hzblj/kara-native/swift-ui'
3
+ import {spacing, useAppTheme} from '@hzblj/kara-theme'
4
+ import {useMemo} from 'react'
5
+
6
+ import type {GlyphComponent, GlyphProps} from './glyph.types'
7
+
8
+ /** The SF Symbol in a `UIImageView`, drawn at the size `Glyph` gives it inside a host — the React Native tier. */
9
+ export const RnGlyph: GlyphComponent = ({color = 'content.primary', name, size = 's24'}: GlyphProps) => {
10
+ const theme = useAppTheme()
11
+ const style = useMemo(() => ({height: spacing[size], width: spacing[size]}), [size])
12
+
13
+ return (
14
+ <IconView iconColor={theme.color(color)} style={style} symbolSize={spacing[size]} systemName={GLYPHS[name].ios} />
15
+ )
16
+ }
@@ -2,7 +2,7 @@ import {AnimatedVisibility, Column, Text as ComposeText, Row} from '@expo/ui/jet
2
2
  import {clickable} from '@expo/ui/jetpack-compose/modifiers'
3
3
  import {useAppTheme} from '@hzblj/kara-theme'
4
4
  import {useMemo} from 'react'
5
- import {Icon} from '../../atoms/icon'
5
+ import {Glyph} from '../../glyph'
6
6
  import {useHostGuard} from '../../host-context'
7
7
  import type {CollapsibleComponent, CollapsibleProps} from './collapsible.types'
8
8
 
@@ -17,7 +17,7 @@ export const Collapsible: CollapsibleComponent = ({children, expanded, label, on
17
17
  <ComposeText color={theme.colorString('content.primary')} style={theme.typography.body}>
18
18
  {label}
19
19
  </ComposeText>
20
- <Icon name={expanded ? 'chevronDown' : 'chevronRight'} size="s20" />
20
+ <Glyph name={expanded ? 'chevronDown' : 'chevronRight'} size="s20" />
21
21
  </Row>
22
22
  <AnimatedVisibility visible={expanded}>{children}</AnimatedVisibility>
23
23
  </Column>
@@ -2,6 +2,7 @@ import {HStack} from '../../atoms/h-stack'
2
2
  import {Icon} from '../../atoms/icon'
3
3
  import {Spacer} from '../../atoms/spacer'
4
4
  import {Text} from '../../atoms/text'
5
+ import {Glyph} from '../../glyph'
5
6
  import type {LabeledContentComponent, LabeledContentProps} from './labeled-content.types'
6
7
 
7
8
  const TrailingValue = ({value}: {value: string}) => (
@@ -23,6 +24,6 @@ export const LabeledContent: LabeledContentComponent = ({
23
24
  <Text variant="body">{label}</Text>
24
25
  <Spacer />
25
26
  {children ?? (value === undefined ? null : <TrailingValue value={value} />)}
26
- {onPress ? <Icon color="content.quaternary" name="chevronRight" size="s16" /> : null}
27
+ {onPress ? <Glyph color="content.quaternary" name="chevronRight" size="s16" /> : null}
27
28
  </HStack>
28
29
  )
@@ -3,6 +3,7 @@ import {Icon} from '../../atoms/icon'
3
3
  import {Spacer} from '../../atoms/spacer'
4
4
  import {Text} from '../../atoms/text'
5
5
  import {VStack} from '../../atoms/v-stack'
6
+ import {Glyph} from '../../glyph'
6
7
  import type {ListItemComponent, ListItemProps} from './list-item.types'
7
8
 
8
9
  export const ListItem: ListItemComponent = ({icon, onPress, subtitle, testID, title}: ListItemProps) => (
@@ -17,6 +18,6 @@ export const ListItem: ListItemComponent = ({icon, onPress, subtitle, testID, ti
17
18
  ) : null}
18
19
  </VStack>
19
20
  <Spacer />
20
- {onPress ? <Icon color="content.quaternary" name="chevronRight" size="s16" /> : null}
21
+ {onPress ? <Glyph color="content.quaternary" name="chevronRight" size="s16" /> : null}
21
22
  </HStack>
22
23
  )
@@ -1,21 +1,28 @@
1
1
  import {HStack} from '../../atoms/h-stack'
2
- import {IconButton} from '../../atoms/icon-button'
3
2
  import {Island} from '../../atoms/island'
4
3
  import {Spacer} from '../../atoms/spacer'
5
4
  import {Text} from '../../atoms/text'
5
+ import {GlyphButton} from '../../glyph'
6
6
  import type {NavigationBarComponent, NavigationBarProps} from './navigation-bar.types'
7
7
 
8
8
  /**
9
9
  * Material's top bar shape — navigation icon, start-aligned title, action at the end — built from the atoms in its own
10
10
  * island. Compose's icon buttons take no accessibility identifier, so `backTestID`/`closeTestID` are iOS-only.
11
11
  */
12
- export const NavigationBar: NavigationBarComponent = ({onBack, onClose, testID, title}: NavigationBarProps) => (
12
+ export const NavigationBar: NavigationBarComponent = ({
13
+ backLabel = 'Back',
14
+ closeLabel = 'Close',
15
+ onBack,
16
+ onClose,
17
+ testID,
18
+ title,
19
+ }: NavigationBarProps) => (
13
20
  <Island testID={testID}>
14
21
  <HStack align="center" px="s4" spacing="s4" wide>
15
- {onBack ? <IconButton name="arrowLeft" onPress={onBack} /> : null}
22
+ {onBack ? <GlyphButton accessibilityLabel={backLabel} name="back" onPress={onBack} /> : null}
16
23
  {title ? <Text variant="body">{title}</Text> : null}
17
24
  <Spacer />
18
- {onClose ? <IconButton name="close" onPress={onClose} /> : null}
25
+ {onClose ? <GlyphButton accessibilityLabel={closeLabel} name="close" onPress={onClose} /> : null}
19
26
  </HStack>
20
27
  </Island>
21
28
  )
@@ -1,4 +1,5 @@
1
1
  import {Text as ComposeText, RadioButton, Row} from '@expo/ui/jetpack-compose'
2
+ import {clickable, fillMaxWidth} from '@expo/ui/jetpack-compose/modifiers'
2
3
  import {useAppTheme} from '@hzblj/kara-theme'
3
4
  import {VStack} from '../../atoms/v-stack'
4
5
  import {useHostGuard} from '../../host-context'
@@ -12,8 +13,16 @@ export const RadioGroup: RadioGroupComponent = ({onValueChange, options, value}:
12
13
  return (
13
14
  <VStack spacing="s4" wide>
14
15
  {options.map(option => (
15
- <Row key={option.value} verticalAlignment="center">
16
- <RadioButton onClick={() => onValueChange?.(option.value)} selected={option.value === value} />
16
+ <Row
17
+ key={option.value}
18
+ modifiers={[fillMaxWidth(), clickable(() => onValueChange?.(option.value))]}
19
+ verticalAlignment="center"
20
+ >
21
+ <RadioButton
22
+ colors={{selectedColor: theme.color('fill.accent.primary')}}
23
+ onClick={() => onValueChange?.(option.value)}
24
+ selected={option.value === value}
25
+ />
17
26
  <ComposeText color={theme.colorString('content.primary')} style={theme.typography.body}>
18
27
  {option.label}
19
28
  </ComposeText>
@@ -1,8 +1,8 @@
1
1
  import {HStack} from '../../atoms/h-stack'
2
- import {Icon} from '../../atoms/icon'
3
2
  import {Spacer} from '../../atoms/spacer'
4
3
  import {Text} from '../../atoms/text'
5
4
  import {VStack} from '../../atoms/v-stack'
5
+ import {Glyph} from '../../glyph'
6
6
  import {useHostGuard} from '../../host-context'
7
7
  import type {RadioGroupComponent, RadioGroupProps} from './radio-group.types'
8
8
 
@@ -15,7 +15,7 @@ export const RadioGroup: RadioGroupComponent = ({onValueChange, options, value}:
15
15
  <HStack align="center" key={option.value} onPress={() => onValueChange?.(option.value)} py="s8" wide>
16
16
  <Text>{option.label}</Text>
17
17
  <Spacer />
18
- {option.value === value ? <Icon color="content.accent" name="check" size="s20" /> : null}
18
+ {option.value === value ? <Glyph color="content.accent" name="check" size="s20" /> : null}
19
19
  </HStack>
20
20
  ))}
21
21
  </VStack>
@@ -7,6 +7,9 @@ export type {ScrollEdgeEffect, ScrollEdgeScrollableProps} from '@hzblj/kara-nati
7
7
  /**
8
8
  * Bars over one React Native scroll view, with the iOS 26 scroll edge effect under them — the system's progressive
9
9
  * blur where content scrolls past a bar. The React Native tier: it needs no host.
10
+ *
11
+ * A screen-level container: wrap the screen's scroll view with it. Mounted inside a cell of a virtualized list on iOS
12
+ * it draws only its bottom bar — the hosted scroll view and the top bar stay blank.
10
13
  */
11
14
  export type ScrollEdgeContainerProps = ResponsiveViewSx & {
12
15
  /**
@@ -1,10 +1,9 @@
1
- import {spacing, useAppTheme} from '@hzblj/kara-theme'
1
+ import {useAppTheme} from '@hzblj/kara-theme'
2
2
  import {TextInput} from 'react-native'
3
3
 
4
4
  import {Box} from '../../atoms/box'
5
- import {Icon} from '../../atoms/icon'
6
- import {Island} from '../../atoms/island'
7
5
  import {Pressable} from '../../atoms/pressable'
6
+ import {RnGlyph} from '../../glyph'
8
7
  import type {SearchBarComponent, SearchBarProps} from './search-bar.types'
9
8
 
10
9
  export const SearchBar: SearchBarComponent = ({
@@ -27,9 +26,7 @@ export const SearchBar: SearchBarComponent = ({
27
26
  testID={testID}
28
27
  wide
29
28
  >
30
- <Island style={{height: spacing.s20, width: spacing.s20}}>
31
- <Icon color="content.tertiary" name="search" size="s20" />
32
- </Island>
29
+ <RnGlyph color="content.tertiary" name="search" size="s20" />
33
30
  <TextInput
34
31
  onChangeText={onChangeText}
35
32
  placeholder={placeholder}
@@ -39,9 +36,7 @@ export const SearchBar: SearchBarComponent = ({
39
36
  />
40
37
  {value.length > 0 ? (
41
38
  <Pressable onPress={() => onChangeText?.('')}>
42
- <Island style={{height: spacing.s20, width: spacing.s20}}>
43
- <Icon color="content.tertiary" name="close" size="s20" />
44
- </Island>
39
+ <RnGlyph color="content.tertiary" name="close" size="s20" />
45
40
  </Pressable>
46
41
  ) : null}
47
42
  </Box>
@@ -1,8 +1,8 @@
1
1
  import {useCallback} from 'react'
2
2
  import {HStack} from '../../atoms/h-stack'
3
- import {IconButton} from '../../atoms/icon-button'
4
3
  import {Spacer} from '../../atoms/spacer'
5
4
  import {Text} from '../../atoms/text'
5
+ import {GlyphButton} from '../../glyph'
6
6
  import {useHostGuard} from '../../host-context'
7
7
  import type {StepperComponent, StepperProps} from './stepper.types'
8
8
 
@@ -23,9 +23,9 @@ export const Stepper: StepperComponent = ({
23
23
  <HStack align="center" spacing="s8" wide>
24
24
  <Text>{label}</Text>
25
25
  <Spacer />
26
- <IconButton disabled={value <= min} name="close" onPress={onDecrement} />
26
+ <GlyphButton accessibilityLabel="Decrement" disabled={value <= min} name="minus" onPress={onDecrement} />
27
27
  <Text variant="labelMedium">{String(value)}</Text>
28
- <IconButton disabled={value >= max} name="add" onPress={onIncrement} />
28
+ <GlyphButton accessibilityLabel="Increment" disabled={value >= max} name="plus" onPress={onIncrement} />
29
29
  </HStack>
30
30
  )
31
31
  }