@helpwave/hightide-native 0.0.1 → 0.0.2

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 (58) hide show
  1. package/README.md +56 -0
  2. package/package.json +3 -3
  3. package/src/components/chat/ChatAttachmentCard.tsx +97 -0
  4. package/src/components/chat/ChatConversationList.tsx +68 -0
  5. package/src/components/chat/ChatConversationRow.tsx +145 -0
  6. package/src/components/chat/ChatDateDivider.tsx +45 -0
  7. package/src/components/chat/ChatMessageBubble.tsx +102 -0
  8. package/src/components/chat/ChatMessageCard.tsx +107 -0
  9. package/src/components/chat/ChatMessageComposer.tsx +110 -0
  10. package/src/components/chat/ChatMessageList.tsx +59 -0
  11. package/src/components/chat/ChatQuickReplyChip.tsx +71 -0
  12. package/src/components/chat/ChatSystemLine.tsx +57 -0
  13. package/src/components/chat/ChatThreadHeader.tsx +80 -0
  14. package/src/components/chat/index.ts +11 -0
  15. package/src/components/index.ts +4 -7
  16. package/src/components/menu/Menu.tsx +51 -0
  17. package/src/components/menu/MenuActionItem.tsx +85 -0
  18. package/src/components/menu/MenuItem.tsx +63 -0
  19. package/src/components/menu/MenuNavigationItem.tsx +85 -0
  20. package/src/components/menu/index.ts +4 -0
  21. package/src/components/{Button → user-interaction}/Button.tsx +22 -29
  22. package/src/components/{Checkbox → user-interaction}/Checkbox.tsx +14 -11
  23. package/src/components/{IconButton → user-interaction}/IconButton.tsx +17 -25
  24. package/src/components/{Input → user-interaction}/Input.tsx +15 -11
  25. package/src/components/{MultiSelect → user-interaction}/MultiSelect.tsx +34 -9
  26. package/src/components/{Select → user-interaction}/Select.tsx +33 -8
  27. package/src/components/user-interaction/index.ts +6 -0
  28. package/src/components/{Chip → visualization-and-display}/Chip.tsx +17 -13
  29. package/src/theme/resolvers/button.ts +7 -5
  30. package/src/theme/resolvers/chat.ts +384 -0
  31. package/src/theme/resolvers/checkbox.ts +5 -4
  32. package/src/theme/resolvers/chip.ts +7 -5
  33. package/src/theme/resolvers/iconButton.ts +6 -4
  34. package/src/theme/resolvers/index.ts +2 -0
  35. package/src/theme/resolvers/input.ts +6 -4
  36. package/src/theme/resolvers/menu.ts +112 -0
  37. package/src/theme/resolvers/multiSelect.ts +20 -19
  38. package/src/theme/resolvers/select.ts +16 -15
  39. package/src/theme/themes/createTheme.ts +4 -0
  40. package/src/theme/types/button.ts +7 -30
  41. package/src/theme/types/chat.ts +141 -0
  42. package/src/theme/types/checkbox.ts +5 -14
  43. package/src/theme/types/chip.ts +7 -30
  44. package/src/theme/types/components.ts +4 -0
  45. package/src/theme/types/iconButton.ts +6 -20
  46. package/src/theme/types/index.ts +2 -0
  47. package/src/theme/types/input.ts +5 -15
  48. package/src/theme/types/menu.ts +50 -0
  49. package/src/theme/types/multiSelect.ts +17 -39
  50. package/src/theme/types/resolver.ts +43 -1
  51. package/src/theme/types/select.ts +18 -55
  52. package/src/components/Button/index.ts +0 -1
  53. package/src/components/Checkbox/index.ts +0 -1
  54. package/src/components/IconButton/index.ts +0 -1
  55. package/src/components/Input/index.ts +0 -1
  56. package/src/components/MultiSelect/index.ts +0 -1
  57. package/src/components/Select/index.ts +0 -1
  58. /package/src/components/{Chip → visualization-and-display}/index.ts +0 -0
@@ -0,0 +1,384 @@
1
+ import { StyleSheet } from 'react-native'
2
+ import {
3
+ hexWithAlpha,
4
+ remToPx,
5
+ type ColoringTokensDefinitions,
6
+ type ComponentColors,
7
+ type DesignTheme as DesignTokensTheme,
8
+ type SemanticColors
9
+ } from '@helpwave/hightide-design'
10
+ import type {
11
+ ChatAttachmentCardState,
12
+ ChatConversationRowState,
13
+ ChatMessageBubbleState,
14
+ ChatMessageCardState,
15
+ ChatQuickReplyChipState,
16
+ ChatSystemLineState,
17
+ ChatTheme
18
+ } from '../types'
19
+ import { createStyleResolver, createValueResolver } from '../types/resolver'
20
+
21
+ export type CreateChatThemeOptions = {
22
+ semantic: SemanticColors,
23
+ component: ComponentColors,
24
+ coloring: ColoringTokensDefinitions,
25
+ }
26
+
27
+ export const createChatTheme = ({
28
+ semantic,
29
+ component,
30
+ coloring,
31
+ }: CreateChatThemeOptions): ChatTheme => {
32
+ const resolveConversationRow = (state: ChatConversationRowState) => {
33
+ const pressed = !!state.isPressed && !state.isDisabled
34
+
35
+ return {
36
+ flexDirection: 'row' as const,
37
+ alignItems: 'center' as const,
38
+ gap: remToPx('0.75rem'),
39
+ width: '100%' as const,
40
+ paddingHorizontal: remToPx('0.875rem'),
41
+ paddingVertical: remToPx('0.75rem'),
42
+ backgroundColor: state.isSelected
43
+ ? semantic.background
44
+ : pressed
45
+ ? semantic.surfaceHover
46
+ : ('transparent' as const),
47
+ borderLeftWidth: state.isSelected ? remToPx('0.25rem') : 0,
48
+ borderLeftColor: state.isSelected ? semantic.primary : ('transparent' as const),
49
+ borderRadius: remToPx('0.375rem'),
50
+ }
51
+ }
52
+
53
+ return {
54
+ conversationRow: createStyleResolver(resolveConversationRow),
55
+ conversationRowTitle: createStyleResolver((state) => ({
56
+ flex: 1,
57
+ color: semantic.onSurface,
58
+ fontSize: remToPx('1rem'),
59
+ fontWeight: state.isUnread ? '700' : '500',
60
+ })),
61
+ conversationRowTimestamp: createStyleResolver((state) => ({
62
+ color: state.isUnread ? semantic.primary : semantic.description,
63
+ fontSize: remToPx('0.75rem'),
64
+ fontWeight: state.isUnread ? '500' : '400',
65
+ flexShrink: 0,
66
+ })),
67
+ conversationRowPreview: createStyleResolver((state) => ({
68
+ flex: 1,
69
+ color: state.isUnread ? semantic.onSurface : semantic.description,
70
+ fontSize: remToPx('0.875rem'),
71
+ fontWeight: '300',
72
+ })),
73
+ conversationRowUnreadBadge: createStyleResolver(() => ({
74
+ minWidth: remToPx('1.25rem'),
75
+ height: remToPx('1.25rem'),
76
+ paddingHorizontal: remToPx('0.375rem'),
77
+ borderRadius: 999,
78
+ backgroundColor: semantic.primary,
79
+ alignItems: 'center',
80
+ justifyContent: 'center',
81
+ })),
82
+ conversationRowUnreadBadgeText: createStyleResolver(() => ({
83
+ color: semantic.onPrimary,
84
+ fontSize: remToPx('0.6875rem'),
85
+ fontWeight: '700',
86
+ })),
87
+ conversationRowSentIndicator: createValueResolver(() => ({
88
+ color: semantic.primary,
89
+ })),
90
+ conversationList: createStyleResolver(() => ({
91
+ flex: 1,
92
+ backgroundColor: semantic.surface,
93
+ })),
94
+ conversationListHeader: createStyleResolver(() => ({
95
+ paddingHorizontal: remToPx('0.875rem'),
96
+ paddingVertical: remToPx('0.875rem'),
97
+ gap: remToPx('0.75rem'),
98
+ })),
99
+ conversationListFooter: createStyleResolver(() => ({
100
+ paddingHorizontal: remToPx('0.875rem'),
101
+ paddingVertical: remToPx('0.5rem'),
102
+ })),
103
+ threadHeader: createStyleResolver(() => ({
104
+ flexDirection: 'row',
105
+ alignItems: 'center',
106
+ gap: remToPx('0.75rem'),
107
+ paddingHorizontal: remToPx('0.875rem'),
108
+ paddingVertical: remToPx('0.75rem'),
109
+ borderBottomWidth: StyleSheet.hairlineWidth,
110
+ borderBottomColor: component.divider,
111
+ backgroundColor: semantic.surface,
112
+ })),
113
+ threadHeaderTitle: createStyleResolver(() => ({
114
+ color: semantic.onSurface,
115
+ fontSize: remToPx('1rem'),
116
+ fontWeight: '700',
117
+ })),
118
+ threadHeaderSubtitle: createStyleResolver(() => ({
119
+ color: semantic.description,
120
+ fontSize: remToPx('0.75rem'),
121
+ fontWeight: '300',
122
+ })),
123
+ messageList: createStyleResolver(() => ({
124
+ flex: 1,
125
+ paddingHorizontal: remToPx('1rem'),
126
+ paddingVertical: remToPx('1.125rem'),
127
+ gap: remToPx('0.75rem'),
128
+ backgroundColor: semantic.background,
129
+ })),
130
+ messageBubbleContainer: createStyleResolver((state: ChatMessageBubbleState) => ({
131
+ maxWidth: remToPx('17.5rem'),
132
+ gap: remToPx('0.25rem'),
133
+ alignSelf: state.direction === 'outgoing' ? 'flex-end' : 'flex-start',
134
+ alignItems: state.direction === 'outgoing' ? 'flex-end' : 'flex-start',
135
+ })),
136
+ messageBubble: createStyleResolver((state: ChatMessageBubbleState) => {
137
+ const outgoing = state.direction === 'outgoing'
138
+ const radius = remToPx('0.75rem')
139
+ const corner = remToPx('0.25rem')
140
+
141
+ return {
142
+ paddingHorizontal: remToPx('0.9375rem'),
143
+ paddingVertical: remToPx('0.6875rem'),
144
+ backgroundColor: outgoing ? semantic.primary : semantic.neutral,
145
+ borderTopLeftRadius: radius,
146
+ borderTopRightRadius: radius,
147
+ borderBottomLeftRadius: outgoing ? radius : corner,
148
+ borderBottomRightRadius: outgoing ? corner : radius,
149
+ }
150
+ }),
151
+ messageBubbleContent: createStyleResolver((state: ChatMessageBubbleState) => ({
152
+ color: state.direction === 'outgoing' ? semantic.onPrimary : semantic.onNeutral,
153
+ fontSize: remToPx('1rem'),
154
+ fontWeight: '300',
155
+ lineHeight: remToPx('1.4rem'),
156
+ })),
157
+ messageBubbleTimestamp: createStyleResolver((state: ChatMessageBubbleState) => ({
158
+ marginTop: remToPx('0.3125rem'),
159
+ color: state.direction === 'outgoing'
160
+ ? hexWithAlpha(semantic.onPrimary, 0.75)
161
+ : semantic.description,
162
+ fontSize: remToPx('0.6875rem'),
163
+ fontWeight: '500',
164
+ textAlign: 'right',
165
+ })),
166
+ messageBubbleReceipt: createStyleResolver(() => ({
167
+ flexDirection: 'row',
168
+ alignItems: 'center',
169
+ gap: remToPx('0.3125rem'),
170
+ })),
171
+ messageBubbleReceiptText: createStyleResolver(() => ({
172
+ color: semantic.description,
173
+ fontSize: remToPx('0.6875rem'),
174
+ fontWeight: '500',
175
+ })),
176
+ messageBubbleReceiptIcon: createValueResolver(() => ({
177
+ color: semantic.primary,
178
+ })),
179
+ messageCard: createStyleResolver((state: ChatMessageCardState) => {
180
+ const outgoing = state.direction === 'outgoing'
181
+ const radius = remToPx('0.75rem')
182
+ const corner = remToPx('0.25rem')
183
+
184
+ return {
185
+ width: remToPx('18.125rem'),
186
+ maxWidth: remToPx('18.75rem'),
187
+ backgroundColor: semantic.surface,
188
+ borderWidth: 1,
189
+ borderColor: component.divider,
190
+ borderTopLeftRadius: radius,
191
+ borderTopRightRadius: radius,
192
+ borderBottomLeftRadius: outgoing ? radius : corner,
193
+ borderBottomRightRadius: outgoing ? corner : radius,
194
+ overflow: 'hidden',
195
+ alignSelf: outgoing ? 'flex-end' : 'flex-start',
196
+ }
197
+ }),
198
+ messageCardHeader: createStyleResolver(() => ({
199
+ flexDirection: 'row',
200
+ alignItems: 'center',
201
+ gap: remToPx('0.625rem'),
202
+ paddingHorizontal: remToPx('0.9375rem'),
203
+ paddingVertical: remToPx('0.75rem'),
204
+ borderBottomWidth: StyleSheet.hairlineWidth,
205
+ borderBottomColor: component.divider,
206
+ })),
207
+ messageCardIcon: createStyleResolver((state: ChatMessageCardState) => {
208
+ const color = state.color ?? 'primary'
209
+ const tokens = coloring[color]
210
+
211
+ return {
212
+ width: remToPx('2.25rem'),
213
+ height: remToPx('2.25rem'),
214
+ borderRadius: remToPx('0.375rem'),
215
+ alignItems: 'center',
216
+ justifyContent: 'center',
217
+ backgroundColor: hexWithAlpha(tokens.tonalBackground ?? tokens.color, 0.2),
218
+ }
219
+ }),
220
+ messageCardIconColor: createValueResolver((state: ChatMessageCardState) => {
221
+ const color = state.color ?? 'primary'
222
+ const tokens = coloring[color]
223
+
224
+ return {
225
+ color: tokens.tonalText ?? tokens.color,
226
+ }
227
+ }),
228
+ messageCardTitle: createStyleResolver((state: ChatMessageCardState) => {
229
+ const color = state.color ?? 'primary'
230
+ const tokens = coloring[color]
231
+
232
+ return {
233
+ color: tokens.text ?? tokens.color,
234
+ fontSize: remToPx('0.875rem'),
235
+ fontWeight: '700',
236
+ }
237
+ }),
238
+ messageCardSubtitle: createStyleResolver(() => ({
239
+ color: semantic.description,
240
+ fontSize: remToPx('0.75rem'),
241
+ })),
242
+ messageCardBody: createStyleResolver(() => ({
243
+ paddingHorizontal: remToPx('0.9375rem'),
244
+ paddingVertical: remToPx('0.75rem'),
245
+ gap: remToPx('0.25rem'),
246
+ })),
247
+ messageCardActions: createStyleResolver(() => ({
248
+ flexDirection: 'row',
249
+ gap: remToPx('0.625rem'),
250
+ paddingHorizontal: remToPx('0.9375rem'),
251
+ paddingBottom: remToPx('0.9375rem'),
252
+ })),
253
+ attachmentCard: createStyleResolver((state: ChatAttachmentCardState) => {
254
+ const outgoing = state.direction === 'outgoing'
255
+ const radius = remToPx('0.75rem')
256
+ const corner = remToPx('0.25rem')
257
+
258
+ return {
259
+ flexDirection: 'row',
260
+ alignItems: 'center',
261
+ gap: remToPx('0.75rem'),
262
+ maxWidth: remToPx('17.5rem'),
263
+ padding: remToPx('0.75rem'),
264
+ backgroundColor: semantic.surface,
265
+ borderWidth: 1,
266
+ borderColor: component.divider,
267
+ borderTopLeftRadius: radius,
268
+ borderTopRightRadius: radius,
269
+ borderBottomLeftRadius: outgoing ? radius : corner,
270
+ borderBottomRightRadius: outgoing ? corner : radius,
271
+ alignSelf: outgoing ? 'flex-end' : 'flex-start',
272
+ }
273
+ }),
274
+ attachmentCardIcon: createStyleResolver(() => ({
275
+ width: remToPx('2.75rem'),
276
+ height: remToPx('2.75rem'),
277
+ borderRadius: remToPx('0.375rem'),
278
+ alignItems: 'center',
279
+ justifyContent: 'center',
280
+ backgroundColor: hexWithAlpha(semantic.negative, 0.2),
281
+ })),
282
+ attachmentCardIconColor: createValueResolver(() => ({
283
+ color: semantic.negative,
284
+ })),
285
+ attachmentCardName: createStyleResolver(() => ({
286
+ color: semantic.onSurface,
287
+ fontSize: remToPx('0.875rem'),
288
+ fontWeight: '500',
289
+ })),
290
+ attachmentCardMetadata: createStyleResolver(() => ({
291
+ color: semantic.description,
292
+ fontSize: remToPx('0.75rem'),
293
+ })),
294
+ systemLine: createStyleResolver(() => ({
295
+ flexDirection: 'row',
296
+ alignItems: 'center',
297
+ justifyContent: 'center',
298
+ alignSelf: 'center',
299
+ gap: remToPx('0.375rem'),
300
+ })),
301
+ systemLineText: createStyleResolver((state: ChatSystemLineState) => {
302
+ const color = state.color ?? 'primary'
303
+ const tokens = coloring[color]
304
+
305
+ return {
306
+ color: tokens.text ?? tokens.color,
307
+ fontSize: remToPx('0.75rem'),
308
+ fontWeight: '500',
309
+ }
310
+ }),
311
+ systemLineIcon: createValueResolver((state: ChatSystemLineState) => {
312
+ const color = state.color ?? 'primary'
313
+ const tokens = coloring[color]
314
+
315
+ return {
316
+ color: tokens.text ?? tokens.color,
317
+ }
318
+ }),
319
+ dateDivider: createStyleResolver(() => ({
320
+ alignSelf: 'center',
321
+ paddingHorizontal: remToPx('0.875rem'),
322
+ paddingVertical: remToPx('0.25rem'),
323
+ borderRadius: 999,
324
+ backgroundColor: semantic.surface,
325
+ })),
326
+ dateDividerText: createStyleResolver(() => ({
327
+ color: semantic.description,
328
+ fontSize: remToPx('0.75rem'),
329
+ fontWeight: '500',
330
+ })),
331
+ quickReplyChip: createStyleResolver((state: ChatQuickReplyChipState) => {
332
+ const pressed = !!state.isPressed && !state.isDisabled
333
+
334
+ return {
335
+ flexDirection: 'row',
336
+ alignItems: 'center',
337
+ alignSelf: 'flex-start',
338
+ gap: remToPx('0.375rem'),
339
+ paddingHorizontal: remToPx('0.875rem'),
340
+ paddingVertical: remToPx('0.375rem'),
341
+ borderRadius: 999,
342
+ borderWidth: 1,
343
+ borderColor: state.isActive ? semantic.primary : component.divider,
344
+ backgroundColor: pressed ? semantic.surfaceHover : semantic.surface,
345
+ }
346
+ }),
347
+ quickReplyChipText: createStyleResolver((state: ChatQuickReplyChipState) => ({
348
+ color: state.isActive ? semantic.primary : semantic.description,
349
+ fontSize: remToPx('0.875rem'),
350
+ fontWeight: '500',
351
+ })),
352
+ messageComposer: createStyleResolver(() => ({
353
+ flexDirection: 'row',
354
+ alignItems: 'flex-end',
355
+ width: '100%',
356
+ gap: remToPx('0.5rem'),
357
+ paddingHorizontal: remToPx('0.875rem'),
358
+ paddingVertical: remToPx('0.75rem'),
359
+ backgroundColor: semantic.surface,
360
+ borderTopWidth: StyleSheet.hairlineWidth,
361
+ borderTopColor: component.divider,
362
+ })),
363
+ messageComposerInput: createStyleResolver(() => ({
364
+ flex: 1,
365
+ minHeight: remToPx('2.75rem'),
366
+ maxHeight: remToPx('2.75rem') * 7,
367
+ paddingHorizontal: remToPx('0.75rem'),
368
+ paddingVertical: remToPx('0.75rem'),
369
+ borderRadius: remToPx('0.375rem'),
370
+ backgroundColor: semantic.surfaceVariant,
371
+ color: semantic.onSurface,
372
+ fontSize: remToPx('0.9375rem'),
373
+ })),
374
+ messageComposerPlaceholderColor: createValueResolver(() => semantic.placeholder),
375
+ }
376
+ }
377
+
378
+ export const createChatThemeFromDesign = (theme: DesignTokensTheme): ChatTheme => {
379
+ return createChatTheme({
380
+ semantic: theme.semantic,
381
+ component: theme.component,
382
+ coloring: theme.coloring,
383
+ })
384
+ }
@@ -6,12 +6,13 @@ import {
6
6
  type ElementSize,
7
7
  type SemanticColors
8
8
  } from '@helpwave/hightide-design'
9
+ import type { ViewStyle } from 'react-native'
9
10
  import type {
10
11
  CheckboxSize,
11
12
  CheckboxState,
12
- CheckboxStyle,
13
13
  CheckboxTheme
14
14
  } from '../types'
15
+ import { createStyleResolver, createValueResolver } from '../types/resolver'
15
16
 
16
17
  const checkboxSizes: Record<CheckboxSize, number> = {
17
18
  sm: remToPx('1.25rem'),
@@ -50,7 +51,7 @@ export const createCheckboxTheme = ({
50
51
  ? semantic.disabled
51
52
  : (isActive ? semantic.primary : component.input.background)
52
53
 
53
- const checkbox: CheckboxStyle = {
54
+ const checkbox: ViewStyle = {
54
55
  width: dimension,
55
56
  height: dimension,
56
57
  alignItems: 'center',
@@ -73,8 +74,8 @@ export const createCheckboxTheme = ({
73
74
  }
74
75
 
75
76
  return {
76
- checkbox: (state) => resolveState(state).checkbox,
77
- icon: (state) => resolveState(state).icon,
77
+ checkbox: createStyleResolver((state) => resolveState(state).checkbox),
78
+ icon: createValueResolver((state) => resolveState(state).icon),
78
79
  }
79
80
  }
80
81
 
@@ -5,7 +5,9 @@ import {
5
5
  type DesignTheme as DesignTokensTheme,
6
6
  type SemanticColors
7
7
  } from '@helpwave/hightide-design'
8
- import type { ChipState, ChipStyle, ChipTextStyle, ChipTheme } from '../types'
8
+ import type { TextStyle, ViewStyle } from 'react-native'
9
+ import type { ChipState, ChipTheme } from '../types'
10
+ import { createStyleResolver } from '../types/resolver'
9
11
  import { resolveColoringStyles } from './coloring'
10
12
 
11
13
  export type CreateChipThemeOptions = {
@@ -25,7 +27,7 @@ export const createChipTheme = ({
25
27
  const resolved = resolveColoringStyles(tokens, coloringStyle, semantic, state)
26
28
  const layout = componentLayouts.chip[size]
27
29
 
28
- const chip: ChipStyle = {
30
+ const chip: ViewStyle = {
29
31
  flexDirection: 'row',
30
32
  alignItems: 'center',
31
33
  justifyContent: 'center',
@@ -41,7 +43,7 @@ export const createChipTheme = ({
41
43
  opacity: state.isDisabled ? 0.6 : 1,
42
44
  }
43
45
 
44
- const text: ChipTextStyle = {
46
+ const text: TextStyle = {
45
47
  color: resolved.color,
46
48
  fontSize: toPx(layout.fontSize),
47
49
  fontWeight: '600',
@@ -51,8 +53,8 @@ export const createChipTheme = ({
51
53
  }
52
54
 
53
55
  return {
54
- chip: (state) => resolveState(state).chip,
55
- text: (state) => resolveState(state).text,
56
+ chip: createStyleResolver((state) => resolveState(state).chip),
57
+ text: createStyleResolver((state) => resolveState(state).text),
56
58
  }
57
59
  }
58
60
 
@@ -5,7 +5,9 @@ import {
5
5
  type DesignTheme as DesignTokensTheme,
6
6
  type SemanticColors
7
7
  } from '@helpwave/hightide-design'
8
- import type { IconButtonState, IconButtonStyle, IconButtonTheme } from '../types'
8
+ import type { ViewStyle } from 'react-native'
9
+ import type { IconButtonState, IconButtonTheme } from '../types'
10
+ import { createStyleResolver, createValueResolver } from '../types/resolver'
9
11
  import { isOutlineColoringStyle, resolveColoringStyles } from './coloring'
10
12
 
11
13
  export type CreateIconButtonThemeOptions = {
@@ -30,7 +32,7 @@ export const createIconButtonTheme = ({
30
32
  ? resolved.borderWidth
31
33
  : (isOutlineColoringStyle(coloringStyle) ? outlineWidth : 0)
32
34
 
33
- const button: IconButtonStyle = {
35
+ const button: ViewStyle = {
34
36
  alignItems: 'center',
35
37
  justifyContent: 'center',
36
38
  backgroundColor: resolved.backgroundColor,
@@ -49,8 +51,8 @@ export const createIconButtonTheme = ({
49
51
  }
50
52
 
51
53
  return {
52
- button: (state) => resolveState(state).button,
53
- icon: (state) => resolveState(state).icon,
54
+ button: createStyleResolver((state) => resolveState(state).button),
55
+ icon: createValueResolver((state) => resolveState(state).icon),
54
56
  }
55
57
  }
56
58
 
@@ -1,8 +1,10 @@
1
1
  export * from './button'
2
+ export * from './chat'
2
3
  export * from './checkbox'
3
4
  export * from './chip'
4
5
  export * from './coloring'
5
6
  export * from './iconButton'
6
7
  export * from './input'
8
+ export * from './menu'
7
9
  export * from './multiSelect'
8
10
  export * from './select'
@@ -5,7 +5,9 @@ import {
5
5
  type DesignTheme as DesignTokensTheme,
6
6
  type SemanticColors
7
7
  } from '@helpwave/hightide-design'
8
- import type { InputState, InputStyle, InputTheme } from '../types'
8
+ import type { TextStyle } from 'react-native'
9
+ import type { InputState, InputTheme } from '../types'
10
+ import { createStyleResolver, createValueResolver } from '../types/resolver'
9
11
 
10
12
  export type CreateInputThemeOptions = {
11
13
  semantic: SemanticColors,
@@ -16,7 +18,7 @@ export const createInputTheme = ({
16
18
  semantic,
17
19
  component,
18
20
  }: CreateInputThemeOptions): InputTheme => {
19
- const resolveInput = (state: InputState): InputStyle => {
21
+ const resolveInput = (state: InputState): TextStyle => {
20
22
  const sizing = inputElementSizes.md
21
23
  const borderColor = state.isInvalid ? semantic.negative : component.border
22
24
 
@@ -35,8 +37,8 @@ export const createInputTheme = ({
35
37
  }
36
38
 
37
39
  return {
38
- input: resolveInput,
39
- placeholderColor: () => semantic.placeholder,
40
+ input: createStyleResolver(resolveInput),
41
+ placeholderColor: createValueResolver(() => semantic.placeholder),
40
42
  }
41
43
  }
42
44
 
@@ -0,0 +1,112 @@
1
+ import { StyleSheet } from 'react-native'
2
+ import {
3
+ remToPx,
4
+ type ComponentColors,
5
+ type DesignTheme as DesignTokensTheme,
6
+ type SemanticColors
7
+ } from '@helpwave/hightide-design'
8
+ import type { MenuActionItemState, MenuTheme } from '../types'
9
+ import { createStyleResolver, createValueResolver } from '../types/resolver'
10
+
11
+ export type CreateMenuThemeOptions = {
12
+ semantic: SemanticColors,
13
+ component: ComponentColors,
14
+ }
15
+
16
+ export const createMenuTheme = ({
17
+ semantic,
18
+ component,
19
+ }: CreateMenuThemeOptions): MenuTheme => {
20
+ const resolveActionItem = (state: MenuActionItemState) => {
21
+ const pressed = !!state.isPressed && !state.isDisabled
22
+
23
+ return {
24
+ flexDirection: 'row' as const,
25
+ alignItems: 'center' as const,
26
+ minHeight: 64,
27
+ gap: remToPx('0.75rem'),
28
+ paddingHorizontal: remToPx('1rem'),
29
+ paddingVertical: remToPx('0.5rem'),
30
+ borderBottomWidth: StyleSheet.hairlineWidth,
31
+ borderBottomColor: component.divider,
32
+ backgroundColor: pressed ? semantic.surfaceHover : ('transparent' as const),
33
+ opacity: state.isDisabled ? 0.6 : 1,
34
+ }
35
+ }
36
+
37
+ const resolveItemContent = () => ({
38
+ flex: 1,
39
+ gap: remToPx('0.25rem'),
40
+ justifyContent: 'center' as const,
41
+ })
42
+
43
+ const resolveActionLabel = (state: MenuActionItemState) => ({
44
+ color: state.isDanger ? semantic.negative : semantic.onSurface,
45
+ fontSize: remToPx('0.9375rem'),
46
+ fontWeight: '500' as const,
47
+ })
48
+
49
+ const resolveActionIcon = (state: MenuActionItemState) => ({
50
+ color: state.isDanger ? semantic.negative : semantic.primary,
51
+ })
52
+
53
+ return {
54
+ section: createStyleResolver(() => ({
55
+ marginBottom: remToPx('1.25rem'),
56
+ gap: remToPx('0.5rem'),
57
+ })),
58
+ sectionTitle: createStyleResolver(() => ({
59
+ color: semantic.description,
60
+ fontSize: remToPx('0.75rem'),
61
+ fontWeight: '700',
62
+ letterSpacing: 0.4,
63
+ textTransform: 'uppercase',
64
+ paddingHorizontal: remToPx('0.25rem'),
65
+ })),
66
+ card: createStyleResolver(() => ({
67
+ backgroundColor: semantic.surface,
68
+ borderRadius: remToPx('0.75rem'),
69
+ borderWidth: 1,
70
+ borderColor: component.border,
71
+ overflow: 'hidden',
72
+ })),
73
+ item: createStyleResolver(() => ({
74
+ flexDirection: 'row' as const,
75
+ alignItems: 'center' as const,
76
+ minHeight: 64,
77
+ paddingHorizontal: remToPx('1rem'),
78
+ paddingVertical: remToPx('0.5rem'),
79
+ borderBottomWidth: StyleSheet.hairlineWidth,
80
+ borderBottomColor: component.divider,
81
+ gap: remToPx('0.75rem'),
82
+ })),
83
+ itemContent: createStyleResolver(resolveItemContent),
84
+ itemLabel: createStyleResolver(() => ({
85
+ color: semantic.description,
86
+ fontSize: remToPx('0.75rem'),
87
+ })),
88
+ itemValue: createStyleResolver(() => ({
89
+ color: semantic.onSurface,
90
+ fontSize: remToPx('0.9375rem'),
91
+ fontWeight: '500',
92
+ })),
93
+ actionItem: createStyleResolver(resolveActionItem),
94
+ actionItemContent: createStyleResolver(resolveItemContent),
95
+ actionItemLabel: createStyleResolver(resolveActionLabel),
96
+ actionItemIcon: createValueResolver(resolveActionIcon),
97
+ navigationItem: createStyleResolver(resolveActionItem),
98
+ navigationItemContent: createStyleResolver(resolveItemContent),
99
+ navigationItemLabel: createStyleResolver(resolveActionLabel),
100
+ navigationItemIcon: createValueResolver(resolveActionIcon),
101
+ navigationItemTrailing: createValueResolver(() => ({
102
+ color: semantic.description,
103
+ })),
104
+ }
105
+ }
106
+
107
+ export const createMenuThemeFromDesign = (theme: DesignTokensTheme): MenuTheme => {
108
+ return createMenuTheme({
109
+ semantic: theme.semantic,
110
+ component: theme.component,
111
+ })
112
+ }