@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
@@ -8,7 +8,7 @@ import type { MenuAction } from '../menu';
8
8
  */
9
9
  export type FabMenuProps = {
10
10
  actions: MenuAction[];
11
- /** The closed button's icon. `add` by default. */
11
+ /** The closed button's icon — one of the app's. A plus by default. */
12
12
  icon?: IconName;
13
13
  testID?: string;
14
14
  };
@@ -1,4 +1,9 @@
1
1
  import type { ReactNode } from 'react';
2
+ /**
3
+ * A settings form — SwiftUI `Form` with its grouped sections, Material cards on Android. On iOS it scrolls like a
4
+ * `List`, so it needs a height from outside: a `Screen`, or an `Island` with `matchContents={false}` and a height. In
5
+ * an island that sizes itself to its content it has no height and shows nothing.
6
+ */
2
7
  export type FormProps = {
3
8
  children: ReactNode;
4
9
  testID?: string;
@@ -4,7 +4,6 @@ export * from './context-menu';
4
4
  export * from './fab-menu';
5
5
  export * from './form';
6
6
  export * from './menu';
7
- export * from './rn-context-menu';
8
7
  export * from './sheet';
9
8
  export * from './snackbar';
10
9
  export * from './swipe-actions';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzblj/kara-components",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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,14 +59,13 @@
59
59
  ]
60
60
  },
61
61
  "dependencies": {
62
- "@hzblj/kara-native": "^0.1.0",
63
- "@hzblj/kara-sx": "^0.1.0",
64
- "@hzblj/kara-theme": "^0.1.0",
62
+ "@hzblj/kara-native": "^0.2.1",
63
+ "@hzblj/kara-sx": "^0.2.1",
64
+ "@hzblj/kara-theme": "^0.2.1",
65
65
  "sf-symbols-typescript": "^2.2.0"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@expo/ui": "~57.0.0",
69
- "@expo/vector-icons": "*",
70
69
  "@shopify/flash-list": ">=2.0.0",
71
70
  "expo": "~57.0.0",
72
71
  "expo-haptics": "*",
@@ -77,7 +76,6 @@
77
76
  },
78
77
  "devDependencies": {
79
78
  "@expo/ui": "~57.0.18",
80
- "@expo/vector-icons": "^15.1.1",
81
79
  "@shopify/flash-list": "^2.3.2",
82
80
  "@storybook/react-native": "^10.6.0",
83
81
  "@types/react": "~19.2.18",
@@ -1,9 +1,10 @@
1
- import {Badge as ComposeBadge} from '@expo/ui/jetpack-compose'
1
+ import {Badge as ComposeBadge, Text as ComposeText} from '@expo/ui/jetpack-compose'
2
2
  import {useAppTheme} from '@hzblj/kara-theme'
3
3
  import {useHostGuard} from '../../host-context'
4
4
  import type {BadgeComponent, BadgeProps} from './badge.types'
5
5
  import {BADGE_TOKENS} from './badge-tokens'
6
6
 
7
+ /** Material's badge. Its content is a Compose `Text` — `@expo/ui`'s badge takes composable content, not a string. */
7
8
  export const Badge: BadgeComponent = ({label, tone = 'neutral'}: BadgeProps) => {
8
9
  useHostGuard('Badge')
9
10
  const theme = useAppTheme()
@@ -11,7 +12,9 @@ export const Badge: BadgeComponent = ({label, tone = 'neutral'}: BadgeProps) =>
11
12
 
12
13
  return (
13
14
  <ComposeBadge containerColor={theme.color(tokens.fill)} contentColor={theme.color(tokens.content)}>
14
- {label}
15
+ <ComposeText color={theme.colorString(tokens.content)} style={theme.typography.captionMedium}>
16
+ {label}
17
+ </ComposeText>
15
18
  </ComposeBadge>
16
19
  )
17
20
  }
@@ -33,3 +33,52 @@ export const buttonTokens = (variant: ButtonVariant, disabled: boolean, theme: T
33
33
 
34
34
  return {content: 'btn.content.disabled', fill: disabledFill[variant], shadow: tokens.shadow} as const
35
35
  }
36
+
37
+ /** Relative luminance of a `#rrggbb` colour (any alpha ignored); `undefined` for anything else. */
38
+ const luminance = (color: unknown): number | undefined => {
39
+ const hex = typeof color === 'string' ? /^#([0-9a-f]{6})/i.exec(color)?.[1] : undefined
40
+
41
+ if (!hex) {
42
+ return undefined
43
+ }
44
+
45
+ const [r, g, b] = [0, 2, 4].map(i => {
46
+ const channel = Number.parseInt(hex.slice(i, i + 2), 16) / 255
47
+
48
+ return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4
49
+ })
50
+
51
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
52
+ }
53
+
54
+ /**
55
+ * The hex behind a token in the current appearance. Android resolves an adaptive entry to one; iOS hands the platform
56
+ * a dynamic colour instead, so there the authored entry is read and its branch picked. A system colour has none.
57
+ */
58
+ const currentHex = (token: ColorToken, theme: Theme): unknown => {
59
+ const resolved = theme.color(token)
60
+
61
+ if (typeof resolved === 'string') {
62
+ return resolved
63
+ }
64
+
65
+ const entry = token
66
+ .split(':')[0]
67
+ .split('.')
68
+ .reduce<unknown>((node, key) => (node as Record<string, unknown> | undefined)?.[key], theme)
69
+
70
+ if (entry && typeof entry === 'object' && 'adaptive' in entry) {
71
+ const {dark, light} = (entry as {adaptive: {dark: string; light: string}}).adaptive
72
+
73
+ return theme.dark ? dark : light
74
+ }
75
+
76
+ return entry
77
+ }
78
+
79
+ /**
80
+ * The label for a button filled with an arbitrary `tint`: the on-accent white over a strong colour, the primary
81
+ * content colour over a pale one (`fill.accent.base`) — the light container tokens pair with it in both appearances.
82
+ */
83
+ export const contentOnTint = (tint: ColorToken, theme: Theme): ColorToken =>
84
+ (luminance(currentHex(tint, theme)) ?? 0) > 0.4 ? 'content.primary' : 'content.on.bg'
@@ -1,36 +1,70 @@
1
- import {Button as ComposeButton} from '@expo/ui/jetpack-compose'
1
+ import {Button as ComposeButton, Text as ComposeText, OutlinedButton} 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 {useAppTheme} from '@hzblj/kara-theme'
5
+ import {useMemo} from 'react'
4
6
  import {useHostGuard} from '../../host-context'
5
7
  import type {ButtonComponent, ButtonProps} from './button.types'
6
- import {buttonTokens} from './button-tokens'
8
+ import {buttonTokens, contentOnTint} from './button-tokens'
7
9
 
10
+ /**
11
+ * Material's filled button, in its own stadium shape. No `borderRadius` or `shadow` from the tokens: those shape the
12
+ * SwiftUI button's glossy ring, and laid over a Compose button they clip and outline a rounded rectangle around the
13
+ * pill Material draws. The label is a Compose `Text` — `@expo/ui`'s button takes composable content, not a string.
14
+ *
15
+ * Material has no glass, so `glass` is its outlined button — and a `tint` makes any variant a filled button in that
16
+ * colour, as a prominent style does on iOS. `buttonStyle` and `controlSize` are SwiftUI's alone.
17
+ */
8
18
  export const Button: ButtonComponent = ({
9
19
  disabled = false,
10
20
  label,
11
21
  onPress,
12
22
  testID,
23
+ tint,
13
24
  variant = 'accent',
14
25
  wide = true,
15
26
  }: ButtonProps) => {
16
27
  useHostGuard('Button')
17
28
  const theme = useAppTheme()
18
29
  const tokens = buttonTokens(variant, disabled, theme)
19
- const modifiers = useSxModifiers({borderRadius: 'medium', shadow: tokens.shadow, wide})
30
+ const sxModifiers = useSxModifiers({wide})
31
+ const modifiers = useMemo(
32
+ () => (testID ? [...sxModifiers, testIdModifier(testID)] : sxModifiers),
33
+ [sxModifiers, testID]
34
+ )
35
+ const style = theme.typography.body
36
+ const outlined = variant === 'glass' && !tint
37
+ const fill = tint && !disabled ? tint : tokens.fill
38
+ const content = tint && !disabled ? contentOnTint(tint, theme) : tokens.content
39
+ const MaterialButton = outlined ? OutlinedButton : ComposeButton
20
40
 
21
41
  return (
22
- <ComposeButton
23
- colors={{
24
- containerColor: theme.color(tokens.fill),
25
- contentColor: theme.color(tokens.content),
26
- disabledContainerColor: theme.color(tokens.fill),
27
- disabledContentColor: theme.color(tokens.content),
28
- }}
42
+ <MaterialButton
43
+ colors={
44
+ outlined
45
+ ? {contentColor: theme.color(content), disabledContentColor: theme.color(content)}
46
+ : {
47
+ containerColor: theme.color(fill),
48
+ contentColor: theme.color(content),
49
+ disabledContainerColor: theme.color(fill),
50
+ disabledContentColor: theme.color(content),
51
+ }
52
+ }
29
53
  enabled={!disabled}
30
54
  modifiers={modifiers}
31
55
  onClick={onPress}
32
56
  >
33
- {label}
34
- </ComposeButton>
57
+ <ComposeText
58
+ color={theme.colorString(content)}
59
+ style={{
60
+ fontFamily: style.fontFamily,
61
+ fontSize: style.fontSize,
62
+ fontWeight: style.fontWeight,
63
+ lineHeight: style.lineHeight,
64
+ }}
65
+ >
66
+ {label}
67
+ </ComposeText>
68
+ </MaterialButton>
35
69
  )
36
70
  }
@@ -12,7 +12,7 @@ import {useHostGuard} from '../../host-context'
12
12
  import {Text} from '../text'
13
13
  import {VStack} from '../v-stack'
14
14
  import type {ButtonComponent, ButtonProps, ButtonStyleName, ButtonVariant} from './button.types'
15
- import {buttonTokens} from './button-tokens'
15
+ import {buttonTokens, contentOnTint} from './button-tokens'
16
16
 
17
17
  const STYLE_BY_VARIANT = {
18
18
  accent: 'borderedProminent',
@@ -55,6 +55,9 @@ export const Button: ButtonComponent = ({
55
55
  * button its height — without it a tinted glass button comes out shorter than every other variant.
56
56
  */
57
57
  const glassTint = style === 'glass' && resolvedTint ? theme.color(resolvedTint) : undefined
58
+ const filled = Boolean(glassTint) || style === 'borderedProminent' || style === 'glassProminent'
59
+ // A prominent style — and tinted glass — is coloured by the tint, so a caller's own tint picks the label on it.
60
+ const content = tint && filled && !disabled ? contentOnTint(tint, theme) : tokens.content
58
61
 
59
62
  const modifiers = useMemo(() => {
60
63
  const list = [buttonStyleModifier(glassTint ? 'plain' : style), controlSizeModifier(controlSize)]
@@ -77,7 +80,7 @@ export const Button: ButtonComponent = ({
77
80
  return (
78
81
  <SwiftUIButton modifiers={modifiers} onPress={onPress} testID={testID}>
79
82
  <VStack align="center" py={glassTint ? 's16' : undefined} wide={wide}>
80
- <Text color={tokens.content} variant="body">
83
+ <Text color={content} variant="body">
81
84
  {label}
82
85
  </Text>
83
86
  </VStack>
@@ -1,22 +1,39 @@
1
1
  import {Checkbox as ComposeCheckbox, Row, Text} from '@expo/ui/jetpack-compose'
2
- import {useAppTheme} from '@hzblj/kara-theme'
2
+ import {clickable, fillMaxWidth} from '@expo/ui/jetpack-compose/modifiers'
3
+ import {spacing, useAppTheme} from '@hzblj/kara-theme'
4
+ import {useMemo} from 'react'
3
5
 
4
6
  import {useHostGuard} from '../../host-context'
5
7
  import type {CheckboxComponent, CheckboxProps} from './checkbox.types'
6
8
 
9
+ /**
10
+ * Material's checkbox, tinted with the accent. With a label it is a full-width row that toggles as a whole; the gap is
11
+ * explicit because a checkbox without a handler drops Material's 48dp touch target, and with it the space around it.
12
+ */
7
13
  export const Checkbox: CheckboxComponent = ({disabled = false, label, onValueChange, value}: CheckboxProps) => {
8
14
  useHostGuard('Checkbox')
9
15
  const theme = useAppTheme()
10
- const control = <ComposeCheckbox 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
+ <ComposeCheckbox
22
+ colors={{checkedColor: theme.color('fill.accent.primary'), checkmarkColor: theme.color('content.on.bg')}}
23
+ enabled={!disabled}
24
+ onCheckedChange={onValueChange}
25
+ value={value}
26
+ />
27
+ )
11
28
 
12
29
  if (!label) {
13
30
  return control
14
31
  }
15
32
 
16
33
  return (
17
- <Row verticalAlignment="center">
34
+ <Row horizontalArrangement={{spacedBy: spacing.s8}} modifiers={rowModifiers} verticalAlignment="center">
18
35
  {control}
19
- <Text color={theme.colorString('content.primary')} style={theme.typography.body}>
36
+ <Text color={theme.colorString(disabled ? 'content.tertiary' : 'content.primary')} style={theme.typography.body}>
20
37
  {label}
21
38
  </Text>
22
39
  </Row>
@@ -0,0 +1,74 @@
1
+ import {DateTimePicker} from '@expo/ui/community/datetime-picker'
2
+ import {useAppTheme} from '@hzblj/kara-theme'
3
+ import {useCallback, useState} from 'react'
4
+
5
+ import {Pressable} from '../pressable'
6
+ import {RnText} from '../rn-text'
7
+ import type {DatePickerComponent, DatePickerMode, DatePickerProps} from './date-picker.types'
8
+
9
+ const formatted = (value: Date, mode: DatePickerMode): string => {
10
+ if (mode === 'time') {
11
+ return value.toLocaleTimeString(undefined, {hour: '2-digit', minute: '2-digit'})
12
+ }
13
+
14
+ const date = value.toLocaleDateString(undefined, {day: 'numeric', month: 'short', year: 'numeric'})
15
+
16
+ return mode === 'datetime'
17
+ ? `${date} ${value.toLocaleTimeString(undefined, {hour: '2-digit', minute: '2-digit'})}`
18
+ : date
19
+ }
20
+
21
+ /**
22
+ * The value as a field that opens Material's date picker dialog on a tap. Android's picker *is* a dialog — mounted, it
23
+ * opens — so it is mounted only while open, and unmounted on confirm or dismiss, as the component asks its caller to.
24
+ */
25
+ export const DatePicker: DatePickerComponent = ({
26
+ max,
27
+ min,
28
+ mode = 'date',
29
+ onValueChange,
30
+ testID,
31
+ value,
32
+ }: DatePickerProps) => {
33
+ const theme = useAppTheme()
34
+ const [open, setOpen] = useState(false)
35
+
36
+ const show = useCallback(() => setOpen(true), [])
37
+ const hide = useCallback(() => setOpen(false), [])
38
+
39
+ const onChange = useCallback(
40
+ (_event: unknown, date: Date) => {
41
+ setOpen(false)
42
+ onValueChange?.(date)
43
+ },
44
+ [onValueChange]
45
+ )
46
+
47
+ return (
48
+ <>
49
+ <Pressable
50
+ alignSelf="flex-start"
51
+ bg="fill.tertiary"
52
+ borderRadius="medium"
53
+ onPress={show}
54
+ px="s12"
55
+ py="s8"
56
+ testID={testID}
57
+ >
58
+ <RnText variant="body">{formatted(value, mode)}</RnText>
59
+ </Pressable>
60
+ {open ? (
61
+ <DateTimePicker
62
+ accentColor={theme.colorString('fill.accent.primary')}
63
+ maximumDate={max}
64
+ minimumDate={min}
65
+ mode={mode}
66
+ onDismiss={hide}
67
+ onValueChange={onChange}
68
+ presentation="dialog"
69
+ value={value}
70
+ />
71
+ ) : null}
72
+ </>
73
+ )
74
+ }
@@ -0,0 +1,27 @@
1
+ import {DateTimePicker} from '@expo/ui/community/datetime-picker'
2
+ import {useCallback} from 'react'
3
+
4
+ import type {DatePickerComponent, DatePickerProps} from './date-picker.types'
5
+
6
+ /** The system's compact date picker, inline: a tap on it opens the calendar popover UIKit owns. */
7
+ export const DatePicker: DatePickerComponent = ({
8
+ max,
9
+ min,
10
+ mode = 'date',
11
+ onValueChange,
12
+ testID,
13
+ value,
14
+ }: DatePickerProps) => {
15
+ const onChange = useCallback((_event: unknown, date: Date) => onValueChange?.(date), [onValueChange])
16
+
17
+ return (
18
+ <DateTimePicker
19
+ maximumDate={max}
20
+ minimumDate={min}
21
+ mode={mode}
22
+ onValueChange={onChange}
23
+ testID={testID}
24
+ value={value}
25
+ />
26
+ )
27
+ }
@@ -12,7 +12,10 @@ export const Fab: FabComponent = ({name, onPress}: FabProps) => {
12
12
 
13
13
  return (
14
14
  <FloatingActionButton containerColor={theme.color('fill.accent.primary')} onClick={onPress}>
15
- <Icon color="content.on.bg" name={name} />
15
+ {/* `@expo/ui`'s FAB is slot-based: content outside `.Icon` is not composed at all. */}
16
+ <FloatingActionButton.Icon>
17
+ <Icon color="content.on.bg" name={name} />
18
+ </FloatingActionButton.Icon>
16
19
  </FloatingActionButton>
17
20
  )
18
21
  }
@@ -1,9 +1,12 @@
1
1
  import {Box} from '../box'
2
2
  import type {GlassViewProps} from './glass-view.types'
3
3
 
4
- /** The capsule drawn where there is no Liquid Glass to draw. */
4
+ /**
5
+ * The capsule drawn where there is no Liquid Glass to draw: the glass buttons' own translucent fill, so whatever sits
6
+ * on the glass — usually content meant for a colourful backdrop — stays readable, the way it would on real glass.
7
+ */
5
8
  export const GlassFallback = ({children, interactive: _interactive, tint, ...props}: GlassViewProps) => (
6
- <Box borderRadius="full" {...props} bg={tint ?? 'fill.tertiary'}>
9
+ <Box borderRadius="full" {...props} bg={tint ?? 'btn.bg.glass.base'}>
7
10
  {children}
8
11
  </Box>
9
12
  )
@@ -5,7 +5,7 @@ import type {ReactNode} from 'react'
5
5
  /**
6
6
  * iOS 26 Liquid Glass around a React Native subtree, shaped as a capsule — the React Native tier, so it needs no host
7
7
  * and costs next to nothing in a list cell. Where there is no Liquid Glass (Android, and iOS before 26) it draws a
8
- * capsule of `tint` instead, or of `fill.tertiary` when there is none.
8
+ * capsule of `tint` instead, or of the glass buttons' translucent fill (`btn.bg.glass.base`) when there is none.
9
9
  */
10
10
  export type GlassViewProps = ResponsiveViewSx & {
11
11
  /** React Native views. Give them `pointerEvents="none"` when `interactive`, so the press reaches the `Pressable`. */
@@ -1,17 +1,21 @@
1
- import MaterialIcons from '@expo/vector-icons/MaterialIcons'
1
+ import {iconResourceName} from '@hzblj/kara-native'
2
+ import {DrawableIcon} from '@hzblj/kara-native/jetpack-compose'
2
3
  import {spacing, useAppTheme} from '@hzblj/kara-theme'
4
+
3
5
  import {useHostGuard} from '../../host-context'
4
- import {RnView} from '../rn-view'
5
6
  import type {IconComponent, IconProps} from './icon.types'
6
- import {MATERIAL_ICONS} from './icon-names'
7
7
 
8
- export const Icon: IconComponent = ({color = 'content.primary', name, size = 's24', testID}: IconProps) => {
8
+ /** The app's icon as the vector drawable the config plugin generated from the SVG, drawn by Material's `Icon`. */
9
+ export const Icon: IconComponent = ({accessibilityLabel, color = 'content.primary', name, size = 's24'}: IconProps) => {
9
10
  useHostGuard('Icon')
10
11
  const theme = useAppTheme()
11
12
 
12
13
  return (
13
- <RnView>
14
- <MaterialIcons color={theme.color(color)} name={MATERIAL_ICONS[name]} size={spacing[size]} testID={testID} />
15
- </RnView>
14
+ <DrawableIcon
15
+ accessibilityLabel={accessibilityLabel}
16
+ iconColor={theme.color(color)}
17
+ iconSize={spacing[size]}
18
+ resourceName={iconResourceName(name)}
19
+ />
16
20
  )
17
21
  }
@@ -1,15 +1,35 @@
1
1
  import {Image} from '@expo/ui/swift-ui'
2
+ import {accessibilityHidden, frame, accessibilityLabel as labelModifier, resizable} from '@expo/ui/swift-ui/modifiers'
3
+ import {iconResourceName} from '@hzblj/kara-native'
2
4
  import {spacing, useAppTheme} from '@hzblj/kara-theme'
3
- import type {SFSymbol} from 'sf-symbols-typescript'
5
+ import {useMemo} from 'react'
6
+
4
7
  import {useHostGuard} from '../../host-context'
5
8
  import type {IconComponent, IconProps} from './icon.types'
6
- import {SF_SYMBOLS} from './icon-names'
7
9
 
8
- export const Icon: IconComponent = ({color = 'content.primary', name, size = 's24', testID}: IconProps) => {
10
+ /**
11
+ * The app's icon from its asset catalog — the template image set the config plugin generated from the SVG, vector data
12
+ * kept, so `resizable()` scales it crisply to the token size and the platform tints it.
13
+ */
14
+ export const Icon: IconComponent = ({
15
+ accessibilityLabel,
16
+ color = 'content.primary',
17
+ name,
18
+ size = 's24',
19
+ testID,
20
+ }: IconProps) => {
9
21
  useHostGuard('Icon')
10
22
  const theme = useAppTheme()
23
+ const side = spacing[size]
11
24
 
12
- return (
13
- <Image color={theme.color(color)} size={spacing[size]} systemName={SF_SYMBOLS[name] as SFSymbol} testID={testID} />
25
+ const modifiers = useMemo(
26
+ () => [
27
+ resizable(),
28
+ frame({height: side, width: side}),
29
+ accessibilityLabel ? labelModifier(accessibilityLabel) : accessibilityHidden(true),
30
+ ],
31
+ [accessibilityLabel, side]
14
32
  )
33
+
34
+ return <Image assetName={iconResourceName(name)} color={theme.color(color)} modifiers={modifiers} testID={testID} />
15
35
  }
@@ -1,32 +1,26 @@
1
1
  import type {ColorToken, SpaceSize} from '@hzblj/kara-theme'
2
2
  import type {ReactNode} from 'react'
3
3
 
4
- export type IconName =
5
- | 'add'
6
- | 'arrowLeft'
7
- | 'arrowRight'
8
- | 'calendar'
9
- | 'check'
10
- | 'chevronDown'
11
- | 'chevronRight'
12
- | 'close'
13
- | 'error'
14
- | 'grid'
15
- | 'heart'
16
- | 'home'
17
- | 'info'
18
- | 'lock'
19
- | 'more'
20
- | 'notification'
21
- | 'search'
22
- | 'settings'
23
- | 'share'
24
- | 'star'
25
- | 'trash'
26
- | 'user'
27
- | 'warning'
4
+ declare global {
5
+ /**
6
+ * The app's icon names, one key per SVG in its icon folder. Written by the config plugin
7
+ * (`plugins: [['@hzblj/kara-ui', {icons: {source: './assets/icons'}}]]`) into `kara-icons.d.ts`, or by
8
+ * `npx kara-icons`. Empty until then. Global, so the declaration reaches it without resolving any kara package.
9
+ */
10
+ interface KaraIcons {}
11
+ }
12
+
13
+ type DeclaredIconName = keyof KaraIcons & string
14
+
15
+ /**
16
+ * One of the app's icons. Once the plugin has written `kara-icons.d.ts` only those names type-check — an icon removed
17
+ * from the folder fails at every call site. Before that, any string, so a project can adopt icons gradually.
18
+ */
19
+ export type IconName = [DeclaredIconName] extends [never] ? string : DeclaredIconName
28
20
 
29
21
  export type IconProps = {
22
+ /** What a screen reader says. Unset, the icon is decorative and hidden from it. */
23
+ accessibilityLabel?: string
30
24
  color?: ColorToken
31
25
  name: IconName
32
26
  size?: SpaceSize
@@ -1,3 +1,2 @@
1
1
  export * from './icon'
2
2
  export * from './icon.types'
3
- export * from './icon-names'
@@ -7,7 +7,6 @@ export * from './card'
7
7
  export * from './checkbox'
8
8
  export * from './chip'
9
9
  export * from './color-picker'
10
- export * from './countdown'
11
10
  export * from './date-picker'
12
11
  export * from './divider'
13
12
  export * from './fab'
@@ -23,6 +22,7 @@ export * from './picker'
23
22
  export * from './pressable'
24
23
  export * from './progress'
25
24
  export * from './rn-flat-list'
25
+ export * from './rn-icon'
26
26
  export * from './rn-scroll-view'
27
27
  export * from './rn-section-list'
28
28
  export * from './rn-text'
@@ -1,15 +1,26 @@
1
1
  import {LazyColumn, PullToRefreshBox} from '@expo/ui/jetpack-compose'
2
2
  import {useSxModifiers} from '@hzblj/kara-sx'
3
+ import {Children, Fragment, isValidElement} from 'react'
3
4
  import {useHostGuard} from '../../host-context'
4
5
  import {useRefreshState} from '../../use-refresh-state'
6
+ import {Divider} from '../divider'
5
7
  import type {ListComponent, ListProps} from './list.types'
6
8
 
9
+ /** A divider between rows, the separator SwiftUI's `List` draws on its own — so rows are written the same for both. */
10
+ const withSeparators = (children: ListProps['children']) =>
11
+ Children.toArray(children).map((child, index) => (
12
+ <Fragment key={isValidElement(child) && child.key !== null ? child.key : index}>
13
+ {index > 0 ? <Divider /> : null}
14
+ {child}
15
+ </Fragment>
16
+ ))
17
+
7
18
  export const List: ListComponent = ({children, onRefresh}: ListProps) => {
8
19
  useHostGuard('List')
9
20
  const modifiers = useSxModifiers({wide: true})
10
21
  const {isRefreshing, refresh} = useRefreshState(onRefresh)
11
22
 
12
- const column = <LazyColumn modifiers={modifiers}>{children}</LazyColumn>
23
+ const column = <LazyColumn modifiers={modifiers}>{withSeparators(children)}</LazyColumn>
13
24
 
14
25
  if (!onRefresh) {
15
26
  return column
@@ -1,5 +1,14 @@
1
1
  import type {ReactNode} from 'react'
2
2
 
3
+ /**
4
+ * A scrolling list — SwiftUI `List`, Compose `LazyColumn`. It has no height of its own, so it needs one from outside:
5
+ * a `Screen`, or an `Island` with `matchContents={false}` and a height. In an island that sizes itself to its content
6
+ * Compose measures it against an infinite height and throws ("Vertically scrollable component was measured with an
7
+ * infinity maximum height"); SwiftUI shows nothing. For a handful of rows that should not scroll, use a `VStack`.
8
+ *
9
+ * Rows are separated for you — SwiftUI's own separators, a `Divider` between rows on Android — so a `Divider` among
10
+ * the children is one too many: on iOS it becomes an empty row of its own.
11
+ */
3
12
  export type ListProps = {
4
13
  children: ReactNode
5
14
  /**