nova-design-model-testing 0.0.2 → 0.0.3

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 (170) hide show
  1. package/guidelines/components/action-sheet.md +30 -0
  2. package/guidelines/components/avatar.md +30 -0
  3. package/guidelines/components/badge.md +45 -0
  4. package/guidelines/components/bottom-navigation.md +30 -0
  5. package/guidelines/components/bottom-sheet.md +30 -0
  6. package/guidelines/components/button-group.md +56 -0
  7. package/guidelines/components/button.md +76 -0
  8. package/guidelines/components/card.md +30 -0
  9. package/guidelines/components/checkbox.md +30 -0
  10. package/guidelines/components/chips.md +30 -0
  11. package/guidelines/components/confirm-sheet.md +30 -0
  12. package/guidelines/components/date-picker.md +30 -0
  13. package/guidelines/components/date-wheel.md +30 -0
  14. package/guidelines/components/icon.md +29 -0
  15. package/guidelines/components/inline-notification.md +30 -0
  16. package/guidelines/components/list-item.md +30 -0
  17. package/guidelines/components/list.md +30 -0
  18. package/guidelines/components/loading-indicator.md +30 -0
  19. package/guidelines/components/menu.md +57 -0
  20. package/guidelines/components/passcode.md +35 -0
  21. package/guidelines/components/radio.md +62 -0
  22. package/guidelines/components/tabs.md +30 -0
  23. package/guidelines/components/text-field.md +68 -0
  24. package/guidelines/components/toast.md +30 -0
  25. package/guidelines/components/toggle.md +30 -0
  26. package/guidelines/components/top-navigation.md +30 -0
  27. package/guidelines/foundations/colors.md +43 -0
  28. package/guidelines/foundations/radius.md +16 -0
  29. package/guidelines/foundations/spacing.md +16 -0
  30. package/guidelines/guidelines.md +36 -0
  31. package/guidelines/overview-components.md +16 -0
  32. package/guidelines/overview-icons.md +18 -0
  33. package/guidelines/overview-setup.md +15 -0
  34. package/package.json +1 -1
  35. package/public/vite.svg +1 -0
  36. package/src/App.css +42 -0
  37. package/src/App.jsx +6328 -0
  38. package/src/assets/fonts/inter_bold.otf +0 -0
  39. package/src/assets/fonts/inter_italic.otf +0 -0
  40. package/src/assets/fonts/inter_medium.otf +0 -0
  41. package/src/assets/fonts/inter_regular.otf +0 -0
  42. package/src/assets/fonts/inter_semi_bold.otf +0 -0
  43. package/src/assets/fonts/space_grotesk_bold.otf +0 -0
  44. package/src/assets/fonts/space_grotesk_medium.otf +0 -0
  45. package/src/assets/fonts/space_grotesk_regular.otf +0 -0
  46. package/src/assets/fonts/space_grotesk_semi_bold.otf +0 -0
  47. package/src/assets/icons/Style=Line-1.svg +5 -0
  48. package/src/assets/icons/Style=Line-2.svg +5 -0
  49. package/src/assets/icons/Style=Line-3.svg +5 -0
  50. package/src/assets/icons/Style=Line.svg +5 -0
  51. package/src/assets/icons/Style=Solid.svg +7 -0
  52. package/src/assets/icons/line/Style=Line-1.svg +5 -0
  53. package/src/assets/icons/line/Style=Line-2.svg +5 -0
  54. package/src/assets/icons/line/Style=Line-3.svg +5 -0
  55. package/src/assets/icons/line/Style=Line.svg +5 -0
  56. package/src/assets/icons/line/arrow_circle_down.svg +5 -0
  57. package/src/assets/icons/line/arrow_circle_down_left_ltr.svg +5 -0
  58. package/src/assets/icons/line/arrow_circle_down_right_ltr.svg +5 -0
  59. package/src/assets/icons/line/arrow_circle_left_ltr.svg +5 -0
  60. package/src/assets/icons/line/arrow_circle_right.svg +3 -0
  61. package/src/assets/icons/line/arrow_circle_right_ltr.svg +5 -0
  62. package/src/assets/icons/line/arrow_circle_up.svg +5 -0
  63. package/src/assets/icons/line/arrow_circle_up_left_ltr.svg +5 -0
  64. package/src/assets/icons/line/arrow_circle_up_right_ltr.svg +5 -0
  65. package/src/assets/icons/line/arrow_narrow_down.svg +5 -0
  66. package/src/assets/icons/line/arrow_narrow_down_left_ltr.svg +5 -0
  67. package/src/assets/icons/line/arrow_narrow_down_left_vturn.svg +5 -0
  68. package/src/assets/icons/line/arrow_narrow_down_right_ltr.svg +5 -0
  69. package/src/assets/icons/line/arrow_narrow_left_ltr.svg +5 -0
  70. package/src/assets/icons/line/arrow_narrow_up.svg +5 -0
  71. package/src/assets/icons/line/arrow_narrow_up_left_ltr.svg +5 -0
  72. package/src/assets/icons/line/arrow_narrow_up_right_ltr.svg +5 -0
  73. package/src/assets/icons/line/arrow_turn_down_right_ltr.svg +5 -0
  74. package/src/assets/icons/line/arrows_down.svg +5 -0
  75. package/src/assets/icons/line/arrows_left_ltr.svg +5 -0
  76. package/src/assets/icons/line/arrows_right_ltr.svg +5 -0
  77. package/src/assets/icons/line/arrows_up.svg +5 -0
  78. package/src/assets/icons/line/chevron-selector-vertical.svg +5 -0
  79. package/src/assets/icons/line/chevron_down.svg +5 -0
  80. package/src/assets/icons/line/chevron_down_double.svg +5 -0
  81. package/src/assets/icons/line/chevron_left_double_ltr.svg +5 -0
  82. package/src/assets/icons/line/chevron_left_ltr.svg +5 -0
  83. package/src/assets/icons/line/chevron_right_double_ltr.svg +5 -0
  84. package/src/assets/icons/line/chevron_right_ltr.svg +5 -0
  85. package/src/assets/icons/line/chevron_selector_vertical.svg +5 -0
  86. package/src/assets/icons/line/chevron_up.svg +5 -0
  87. package/src/assets/icons/line/chevron_up_double.svg +5 -0
  88. package/src/assets/icons/line/coin_swap.svg +3 -0
  89. package/src/assets/icons/line/credit_card_edit.svg +5 -0
  90. package/src/assets/icons/line/dev.svg +5 -0
  91. package/src/assets/icons/line/expand_01.svg +5 -0
  92. package/src/assets/icons/line/expand_02.svg +5 -0
  93. package/src/assets/icons/line/flip_backward_ltr.svg +5 -0
  94. package/src/assets/icons/line/flip_forward_ltr.svg +5 -0
  95. package/src/assets/icons/line/home_05.svg +3 -0
  96. package/src/assets/icons/line/infinity.svg +5 -0
  97. package/src/assets/icons/line/minimise.svg +6 -0
  98. package/src/assets/icons/line/moon_01.svg +3 -0
  99. package/src/assets/icons/line/paint.svg +3 -0
  100. package/src/assets/icons/line/plus_circle.svg +3 -0
  101. package/src/assets/icons/line/refresh.svg +5 -0
  102. package/src/assets/icons/line/reset.svg +5 -0
  103. package/src/assets/icons/line/reverse_left_ltr.svg +5 -0
  104. package/src/assets/icons/line/reverse_right_ltr.svg +5 -0
  105. package/src/assets/icons/line/safe_01.svg +3 -0
  106. package/src/assets/icons/line/search.svg +3 -0
  107. package/src/assets/icons/line/shield_star.svg +3 -0
  108. package/src/assets/icons/line/sun.svg +3 -0
  109. package/src/assets/icons/line/switch_horizontal.svg +5 -0
  110. package/src/assets/icons/line/switch_vertical.svg +5 -0
  111. package/src/assets/icons/solid/arrow_circle_right.svg +3 -0
  112. package/src/assets/icons/solid/coin_swap.svg +6 -0
  113. package/src/assets/icons/solid/credit_card_edit.svg +6 -0
  114. package/src/assets/icons/solid/safe_01.svg +5 -0
  115. package/src/assets/images/New user_gold.png +0 -0
  116. package/src/assets/images/Sheet-1.png +0 -0
  117. package/src/assets/images/Sheet.png +0 -0
  118. package/src/assets/logos/botim_credit_dark.svg +22 -0
  119. package/src/assets/logos/botim_credit_light.svg +31 -0
  120. package/src/assets/logos/botim_invest_dark.svg +22 -0
  121. package/src/assets/logos/botim_invest_light.svg +31 -0
  122. package/src/assets/logos/botim_main_dark.svg +18 -0
  123. package/src/assets/logos/botim_main_light.svg +17 -0
  124. package/src/assets/logos/botim_money_dark.svg +26 -0
  125. package/src/assets/logos/botim_money_light.svg +28 -0
  126. package/src/assets/logos/botim_pay_dark.svg +24 -0
  127. package/src/assets/logos/botim_pay_light.svg +35 -0
  128. package/src/assets/react.svg +1 -0
  129. package/src/design-system/components/action-sheet/ActionSheet.jsx +3 -0
  130. package/src/design-system/components/avatar/Avatar.jsx +3 -0
  131. package/src/design-system/components/badge/Badge.jsx +68 -0
  132. package/src/design-system/components/bottom-navigation/BottomNavigation.jsx +3 -0
  133. package/src/design-system/components/bottom-sheet/BottomSheet.jsx +3 -0
  134. package/src/design-system/components/button/Button.jsx +310 -0
  135. package/src/design-system/components/button-group/ButtonGroup.jsx +150 -0
  136. package/src/design-system/components/card/Card.jsx +3 -0
  137. package/src/design-system/components/checkbox/Checkbox.jsx +3 -0
  138. package/src/design-system/components/chips/Chips.jsx +3 -0
  139. package/src/design-system/components/confirm-sheet/ConfirmSheet.jsx +3 -0
  140. package/src/design-system/components/date-picker/DatePicker.jsx +3 -0
  141. package/src/design-system/components/date-wheel/DateWheel.jsx +3 -0
  142. package/src/design-system/components/digit-field/DigitField.jsx +191 -0
  143. package/src/design-system/components/icon/Icon.jsx +154 -0
  144. package/src/design-system/components/inline-notification/InlineNotification.jsx +3 -0
  145. package/src/design-system/components/list/List.jsx +3 -0
  146. package/src/design-system/components/list-item/ListItem.jsx +3 -0
  147. package/src/design-system/components/loading-indicator/LoadingIndicator.jsx +3 -0
  148. package/src/design-system/components/logo/AppLogo.jsx +35 -0
  149. package/src/design-system/components/logo/BrandingLogo.jsx +39 -0
  150. package/src/design-system/components/logo/Logo.jsx +39 -0
  151. package/src/design-system/components/menu/Menu.jsx +100 -0
  152. package/src/design-system/components/menu/MenuItem.jsx +184 -0
  153. package/src/design-system/components/passcode/Passcode.jsx +165 -0
  154. package/src/design-system/components/radio/Radio.jsx +135 -0
  155. package/src/design-system/components/section-header-footer/SectionHeaderFooter.jsx +7 -0
  156. package/src/design-system/components/tabs/Tabs.jsx +3 -0
  157. package/src/design-system/components/text-field/TextField.jsx +308 -0
  158. package/src/design-system/components/toast/Toast.jsx +3 -0
  159. package/src/design-system/components/toggle/Toggle.jsx +3 -0
  160. package/src/design-system/components/top-navigation/TopNavigation.jsx +3 -0
  161. package/src/design-system/theme/ThemeContext.jsx +11 -0
  162. package/src/design-system/tokens/primitives/colors.json +122 -0
  163. package/src/design-system/tokens/primitives/radius.json +9 -0
  164. package/src/design-system/tokens/primitives/sizes.json +10 -0
  165. package/src/design-system/tokens/primitives/spacing.json +10 -0
  166. package/src/design-system/tokens/semantic/colors.dark.json +139 -0
  167. package/src/design-system/tokens/semantic/colors.light.json +139 -0
  168. package/src/design-system/tokens/semantic/spacing.json +18 -0
  169. package/src/index.css +99 -0
  170. package/src/main.jsx +10 -0
@@ -0,0 +1,310 @@
1
+ import { cloneElement, isValidElement, useState } from 'react'
2
+ import primitiveColors from '../../tokens/primitives/colors.json'
3
+ import semanticLight from '../../tokens/semantic/colors.light.json'
4
+ import semanticDark from '../../tokens/semantic/colors.dark.json'
5
+ import { useTheme } from '../../theme/ThemeContext.jsx'
6
+
7
+ const BUTTON_TYPES = {
8
+ primary: 'button/primary',
9
+ secondary: 'button/secondary',
10
+ 'primary-danger': 'button/danger primary',
11
+ 'secondary-danger': 'button/danger secondary',
12
+ }
13
+
14
+ const SIZE_STYLES = {
15
+ small: {
16
+ height: 40,
17
+ fontSize: 14,
18
+ paddingX: 16,
19
+ paddingY: 8,
20
+ },
21
+ large: {
22
+ height: 56,
23
+ fontSize: 16,
24
+ paddingX: 20,
25
+ paddingY: 16,
26
+ },
27
+ }
28
+
29
+ const SIZE_ALIASES = {
30
+ Small: 'small',
31
+
32
+ Large: 'large',
33
+ }
34
+
35
+ const TYPE_ALIASES = {
36
+ Primary: 'primary',
37
+ Secondary: 'secondary',
38
+ Tertiary: 'tertiary',
39
+ 'Primary danger': 'primary-danger',
40
+ 'Secondary danger': 'secondary-danger',
41
+ }
42
+
43
+ const STATE_ALIASES = {
44
+ Default: 'default',
45
+ Pressed: 'pressed',
46
+ Disabled: 'disabled',
47
+ Disable: 'disabled',
48
+ }
49
+
50
+ const ICON_BUTTON_SIZES = {
51
+ small: { size: 40, padding: 10, icon: 20 },
52
+ large: { size: 56, padding: 16, icon: 24 },
53
+ }
54
+
55
+ function resolvePrimitiveReference(reference) {
56
+ if (typeof reference !== 'string' || !reference.startsWith('primitives.')) {
57
+ return reference
58
+ }
59
+
60
+ const path = reference.replace('primitives.', '').split('.')
61
+ let current = primitiveColors
62
+
63
+ for (const key of path) {
64
+ if (!current || typeof current !== 'object') {
65
+ return reference
66
+ }
67
+ current = current[key]
68
+ }
69
+
70
+ return current ?? reference
71
+ }
72
+
73
+ function getSemanticToken(themeTokens, token) {
74
+ const reference = themeTokens[token]
75
+ return resolvePrimitiveReference(reference)
76
+ }
77
+
78
+ function getButtonColors({ themeTokens, type, state, variant }) {
79
+ if (variant === 'text') {
80
+ const textTokens = {
81
+ primary: 'text icon/functional/link',
82
+ secondary: 'text icon/neutral/primary',
83
+ 'primary-danger': 'text icon/functional/danger',
84
+ }
85
+ return {
86
+ background: 'transparent',
87
+ text: getSemanticToken(themeTokens, textTokens[type] || textTokens.primary),
88
+ }
89
+ }
90
+
91
+ if (variant === 'icon') {
92
+ const isPrimary = type === 'primary'
93
+ const isSecondary = type === 'secondary'
94
+ const isPressed = state === 'pressed'
95
+ const backgroundToken = isPrimary
96
+ ? isPressed
97
+ ? 'button/primary/bg-pressed'
98
+ : 'button/primary/bg-default'
99
+ : isPressed
100
+ ? 'background/neutral/tertiary'
101
+ : isSecondary
102
+ ? 'background/neutral/secondary'
103
+ : 'background/neutral/primary'
104
+ const iconToken = isPrimary ? 'button/primary/text-on' : 'text icon/neutral/primary'
105
+ return {
106
+ background: getSemanticToken(themeTokens, backgroundToken),
107
+ text: getSemanticToken(themeTokens, iconToken),
108
+ }
109
+ }
110
+
111
+ const typeKey = BUTTON_TYPES[type] || BUTTON_TYPES.primary
112
+ const backgroundToken =
113
+ state === 'pressed' ? `${typeKey}/bg-pressed` : `${typeKey}/bg-default`
114
+
115
+ return {
116
+ background: getSemanticToken(themeTokens, backgroundToken),
117
+ text: getSemanticToken(themeTokens, `${typeKey}/text-on`),
118
+ }
119
+ }
120
+
121
+ function DotsLoader({ color }) {
122
+ return (
123
+ <span
124
+ aria-hidden="true"
125
+ style={{
126
+ display: 'inline-flex',
127
+ alignItems: 'center',
128
+ gap: 4,
129
+ }}
130
+ >
131
+ {[0, 1, 2].map((index) => (
132
+ <span
133
+ key={index}
134
+ style={{
135
+ width: 6,
136
+ height: 6,
137
+ borderRadius: '50%',
138
+ backgroundColor: color,
139
+ opacity: 0.6 + index * 0.2,
140
+ }}
141
+ />
142
+ ))}
143
+ </span>
144
+ )
145
+ }
146
+
147
+ export default function Button({
148
+ label = 'Button',
149
+ labelText,
150
+ variant = 'default',
151
+ size = 'large',
152
+ type = 'primary',
153
+ state = 'default',
154
+ loading = false,
155
+ showLeadingIcon = true,
156
+ showTrailingIcon = true,
157
+ leadingIcon = null,
158
+ trailingIcon = null,
159
+ icon = null,
160
+ fullWidth = false,
161
+ style: customStyle = {},
162
+ disabled = false,
163
+ onClick,
164
+ ...rest
165
+ }) {
166
+ const [isPressed, setIsPressed] = useState(false)
167
+ const theme = useTheme()
168
+ const themeTokens = theme === 'dark' ? semanticDark : semanticLight
169
+ const normalizedSize = SIZE_ALIASES[size] || size
170
+ const normalizedType = TYPE_ALIASES[type] || type
171
+ const normalizedState = STATE_ALIASES[state] || state
172
+ const buttonSize = SIZE_STYLES[normalizedSize] || SIZE_STYLES.large
173
+ const iconButtonSize = ICON_BUTTON_SIZES[normalizedSize] || ICON_BUTTON_SIZES.small
174
+ const resolvedState = disabled ? 'disabled' : normalizedState
175
+ const allowPress = resolvedState === 'default' && !loading
176
+ const interactiveState = allowPress && isPressed ? 'pressed' : resolvedState
177
+ const colors = getButtonColors({
178
+ themeTokens,
179
+ type: normalizedType,
180
+ state: interactiveState,
181
+ variant,
182
+ })
183
+ const isDisabled = resolvedState === 'disabled' || loading
184
+ const pressedScale = interactiveState === 'pressed' ? 0.97 : 1
185
+ const isFab = variant === 'fab'
186
+ const isIcon = variant === 'icon'
187
+ const isText = variant === 'text'
188
+ const allowFullWidth = fullWidth && !isFab && !isIcon
189
+ const fontSize =
190
+ interactiveState === 'pressed'
191
+ ? Number((buttonSize.fontSize * pressedScale).toFixed(2))
192
+ : buttonSize.fontSize
193
+ const resolvedLabel = labelText ?? label
194
+ const resolvedIcon = icon ?? leadingIcon
195
+ const iconSize = Number((iconButtonSize.icon * pressedScale).toFixed(2))
196
+ const iconProps = {
197
+ size: 20,
198
+ color: 'currentColor',
199
+ }
200
+ const iconContent =
201
+ (isIcon || isFab) && resolvedIcon && isValidElement(resolvedIcon)
202
+ ? cloneElement(resolvedIcon, {
203
+ size: resolvedIcon.props.size ?? (isIcon ? iconSize : 24),
204
+ color: resolvedIcon.props.color ?? 'currentColor',
205
+ })
206
+ : resolvedIcon
207
+
208
+ return (
209
+ <button
210
+ type="button"
211
+ onClick={isDisabled ? undefined : onClick}
212
+ disabled={isDisabled}
213
+ onMouseDown={() => {
214
+ if (allowPress) {
215
+ setIsPressed(true)
216
+ }
217
+ }}
218
+ onMouseUp={() => setIsPressed(false)}
219
+ onMouseLeave={() => setIsPressed(false)}
220
+ onKeyDown={(event) => {
221
+ if (allowPress && (event.key === 'Enter' || event.key === ' ')) {
222
+ setIsPressed(true)
223
+ }
224
+ }}
225
+ onKeyUp={() => setIsPressed(false)}
226
+ style={{
227
+ minHeight: isIcon
228
+ ? Number((iconButtonSize.size * pressedScale).toFixed(2))
229
+ : buttonSize.height,
230
+ minWidth: isIcon
231
+ ? Number((iconButtonSize.size * pressedScale).toFixed(2))
232
+ : undefined,
233
+ height: isIcon
234
+ ? Number((iconButtonSize.size * pressedScale).toFixed(2))
235
+ : undefined,
236
+ width: isIcon
237
+ ? Number((iconButtonSize.size * pressedScale).toFixed(2))
238
+ : allowFullWidth
239
+ ? '100%'
240
+ : undefined,
241
+ padding: isText
242
+ ? 0
243
+ : isIcon
244
+ ? `${(iconButtonSize.padding * pressedScale).toFixed(2)}px`
245
+ : `${(buttonSize.paddingY * pressedScale).toFixed(2)}px ${(
246
+ buttonSize.paddingX * pressedScale
247
+ ).toFixed(2)}px`,
248
+ borderRadius: isIcon
249
+ ? Number((iconButtonSize.size * pressedScale).toFixed(2))
250
+ : Number((32 * pressedScale).toFixed(2)),
251
+ backgroundColor: isText ? 'transparent' : colors.background,
252
+ color: colors.text,
253
+ fontSize,
254
+ fontWeight: 600,
255
+ lineHeight: 1,
256
+ display: 'inline-flex',
257
+ alignItems: 'center',
258
+ justifyContent: 'center',
259
+ gap: 8,
260
+ border: 'none',
261
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
262
+ opacity: isDisabled ? 0.2 : 1,
263
+ alignSelf: isFab || isIcon ? 'flex-start' : allowFullWidth ? 'stretch' : undefined,
264
+ ...customStyle,
265
+ }}
266
+ {...rest}
267
+ >
268
+ {loading ? (
269
+ <DotsLoader color={colors.text} />
270
+ ) : (
271
+ <>
272
+ {isFab || isIcon ? (
273
+ resolvedIcon ? (
274
+ <span
275
+ aria-hidden="true"
276
+ style={{
277
+ width: isIcon ? iconSize : 24,
278
+ height: isIcon ? iconSize : 24,
279
+ display: 'inline-flex',
280
+ }}
281
+ >
282
+ {iconContent}
283
+ </span>
284
+ ) : null
285
+ ) : (
286
+ <>
287
+ {showLeadingIcon && leadingIcon ? (
288
+ <span
289
+ aria-hidden="true"
290
+ style={{ width: 20, height: 20, display: 'inline-flex' }}
291
+ >
292
+ {isValidElement(leadingIcon) ? cloneElement(leadingIcon, iconProps) : leadingIcon}
293
+ </span>
294
+ ) : null}
295
+ <span>{resolvedLabel}</span>
296
+ {showTrailingIcon && trailingIcon ? (
297
+ <span
298
+ aria-hidden="true"
299
+ style={{ width: 20, height: 20, display: 'inline-flex' }}
300
+ >
301
+ {isValidElement(trailingIcon) ? cloneElement(trailingIcon, iconProps) : trailingIcon}
302
+ </span>
303
+ ) : null}
304
+ </>
305
+ )}
306
+ </>
307
+ )}
308
+ </button>
309
+ )
310
+ }
@@ -0,0 +1,150 @@
1
+ import Button from '../button/Button.jsx'
2
+
3
+ const LAYOUT_ALIASES = {
4
+ Vertical: 'vertical',
5
+ Horizontal: 'horizontal',
6
+ 'Horizontal Fill': 'horizontal-fill',
7
+ }
8
+
9
+ const DEFAULT_PRIMARY = {
10
+ variant: 'default',
11
+ size: 'large',
12
+ type: 'primary',
13
+ state: 'default',
14
+ labelText: 'Button',
15
+ showLeadingIcon: true,
16
+ showTrailingIcon: true,
17
+ }
18
+
19
+ const DEFAULT_SECONDARY = {
20
+ variant: 'default',
21
+ size: 'large',
22
+ type: 'secondary',
23
+ state: 'default',
24
+ labelText: 'Button',
25
+ showLeadingIcon: true,
26
+ showTrailingIcon: true,
27
+ }
28
+
29
+ const DEFAULT_TEXT = {
30
+ variant: 'text',
31
+ size: 'large',
32
+ type: 'secondary',
33
+ state: 'default',
34
+ labelText: 'Button',
35
+ showLeadingIcon: true,
36
+ showTrailingIcon: true,
37
+ }
38
+
39
+ export default function ButtonGroup({
40
+ layout = 'vertical',
41
+ showPrimaryButton = true,
42
+ showSecondaryButton1 = true,
43
+ showSecondaryButton2 = true,
44
+ showTextButton = true,
45
+ primaryButtonProps = {},
46
+ secondaryButton1Props = {},
47
+ secondaryButton2Props = {},
48
+ textButtonProps = {},
49
+ }) {
50
+ const normalizedLayout = LAYOUT_ALIASES[layout] || layout
51
+ const isVertical = normalizedLayout === 'vertical'
52
+ const isHorizontalFill = normalizedLayout === 'horizontal-fill'
53
+ const direction = isVertical ? 'column' : 'row'
54
+ const gap = isVertical ? 16 : 12
55
+
56
+ const buttons = []
57
+ if (showPrimaryButton) {
58
+ buttons.push({
59
+ key: 'primary',
60
+ props: { ...DEFAULT_PRIMARY, ...primaryButtonProps },
61
+ })
62
+ }
63
+ if (showSecondaryButton1) {
64
+ buttons.push({
65
+ key: 'secondary-1',
66
+ props: { ...DEFAULT_SECONDARY, ...secondaryButton1Props },
67
+ })
68
+ }
69
+ if (showSecondaryButton2) {
70
+ buttons.push({
71
+ key: 'secondary-2',
72
+ props: { ...DEFAULT_SECONDARY, ...secondaryButton2Props },
73
+ })
74
+ }
75
+ if (showTextButton) {
76
+ buttons.push({
77
+ key: 'text',
78
+ props: { ...DEFAULT_TEXT, ...textButtonProps },
79
+ })
80
+ }
81
+
82
+ return (
83
+ <div
84
+ style={{
85
+ width: '100%',
86
+ display: 'flex',
87
+ flexDirection: 'column',
88
+ alignItems: 'center',
89
+ paddingTop: 8,
90
+ }}
91
+ >
92
+ <div
93
+ style={{
94
+ width: '100%',
95
+ display: 'flex',
96
+ flexDirection: direction,
97
+ gap,
98
+ alignItems: isVertical ? 'stretch' : 'center',
99
+ justifyContent: isVertical ? 'flex-start' : 'center',
100
+ padding: '8px 20px',
101
+ }}
102
+ >
103
+ {buttons.map((button) => {
104
+ const buttonElement = <Button {...button.props} />
105
+ const isText = button.props.variant === 'text'
106
+ const wrappedText =
107
+ isVertical && isText ? (
108
+ <div
109
+ style={{
110
+ width: '100%',
111
+ padding: '12px 20px',
112
+ borderRadius: 32,
113
+ display: 'flex',
114
+ justifyContent: 'center',
115
+ }}
116
+ >
117
+ {buttonElement}
118
+ </div>
119
+ ) : (
120
+ buttonElement
121
+ )
122
+
123
+ if (isHorizontalFill) {
124
+ return (
125
+ <div key={button.key} style={{ flex: 1, display: 'flex' }}>
126
+ {isText ? (
127
+ <div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
128
+ {buttonElement}
129
+ </div>
130
+ ) : (
131
+ <Button {...button.props} fullWidth />
132
+ )}
133
+ </div>
134
+ )
135
+ }
136
+
137
+ if (isVertical && !isText) {
138
+ return (
139
+ <div key={button.key} style={{ width: '100%' }}>
140
+ <Button {...button.props} fullWidth />
141
+ </div>
142
+ )
143
+ }
144
+
145
+ return <div key={button.key}>{wrappedText}</div>
146
+ })}
147
+ </div>
148
+ </div>
149
+ )
150
+ }
@@ -0,0 +1,3 @@
1
+ export const Card = () => {
2
+ return null;
3
+ };
@@ -0,0 +1,3 @@
1
+ export const Checkbox = () => {
2
+ return null;
3
+ };
@@ -0,0 +1,3 @@
1
+ export const Chips = () => {
2
+ return null;
3
+ };
@@ -0,0 +1,3 @@
1
+ export const ConfirmSheet = () => {
2
+ return null;
3
+ };
@@ -0,0 +1,3 @@
1
+ export const DatePicker = () => {
2
+ return null;
3
+ };
@@ -0,0 +1,3 @@
1
+ export const DateWheel = () => {
2
+ return null;
3
+ };
@@ -0,0 +1,191 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import primitiveColors from '../../tokens/primitives/colors.json'
3
+ import semanticLight from '../../tokens/semantic/colors.light.json'
4
+ import semanticDark from '../../tokens/semantic/colors.dark.json'
5
+ import { useTheme } from '../../theme/ThemeContext.jsx'
6
+
7
+ const STATE_ALIASES = {
8
+ Enabled: 'enabled',
9
+ Error: 'error',
10
+ Disabled: 'disabled',
11
+ }
12
+
13
+ const TYPING_ALIASES = {
14
+ 'To type': 'to-type',
15
+ Typing: 'typing',
16
+ Typed: 'typed',
17
+ }
18
+
19
+ function resolvePrimitiveReference(reference) {
20
+ if (typeof reference !== 'string' || !reference.startsWith('primitives.')) {
21
+ return reference
22
+ }
23
+
24
+ const path = reference.replace('primitives.', '').split('.')
25
+ let current = primitiveColors
26
+
27
+ for (const key of path) {
28
+ if (!current || typeof current !== 'object') {
29
+ return reference
30
+ }
31
+ current = current[key]
32
+ }
33
+
34
+ return current ?? reference
35
+ }
36
+
37
+ function getSemanticToken(themeTokens, token) {
38
+ const reference = themeTokens[token]
39
+ return resolvePrimitiveReference(reference)
40
+ }
41
+
42
+ function getDigitFieldColors(themeTokens, state) {
43
+ if (state === 'error') {
44
+ return {
45
+ background: getSemanticToken(themeTokens, 'Input/error/bg'),
46
+ text: getSemanticToken(themeTokens, 'text icon/functional/danger'),
47
+ }
48
+ }
49
+
50
+ if (state === 'disabled') {
51
+ return {
52
+ background: getSemanticToken(themeTokens, 'background/neutral/disabled'),
53
+ text: getSemanticToken(themeTokens, 'text icon/neutral/disabled'),
54
+ }
55
+ }
56
+
57
+ return {
58
+ background: getSemanticToken(themeTokens, 'background/neutral/secondary'),
59
+ text: getSemanticToken(themeTokens, 'text icon/functional/link'),
60
+ }
61
+ }
62
+
63
+ export default function DigitField({
64
+ length = 6,
65
+ value,
66
+ defaultValue = '',
67
+ onChange,
68
+ state = 'enabled',
69
+ typingState = 'to-type',
70
+ enableInteraction = true,
71
+ }) {
72
+ const theme = useTheme()
73
+ const themeTokens = theme === 'dark' ? semanticDark : semanticLight
74
+ const normalizedState = STATE_ALIASES[state] || state
75
+ const normalizedTyping = TYPING_ALIASES[typingState] || typingState
76
+ const isDisabled = normalizedState === 'disabled'
77
+ const isControlled = value !== undefined && value !== null
78
+ const [internalValue, setInternalValue] = useState(defaultValue)
79
+ const inputRef = useRef(null)
80
+ const digitsValue = (isControlled ? value : internalValue) || ''
81
+ const sanitizedValue = String(digitsValue).replace(/\D/g, '').slice(0, length)
82
+ const digits = Array.from({ length }, (_, index) => sanitizedValue[index] || '')
83
+ const [isFocused, setIsFocused] = useState(false)
84
+ const typingActive =
85
+ enableInteraction && isFocused && !isDisabled
86
+ ? 'typing'
87
+ : normalizedTyping
88
+ const showCaret = typingActive === 'typing' && sanitizedValue.length < length
89
+ const caretIndex = Math.min(sanitizedValue.length, length - 1)
90
+ const colors = getDigitFieldColors(themeTokens, normalizedState)
91
+
92
+ useEffect(() => {
93
+ if (!isControlled) {
94
+ setInternalValue(sanitizedValue)
95
+ }
96
+ }, [sanitizedValue, isControlled])
97
+
98
+ const handleChange = (nextValue) => {
99
+ if (!enableInteraction || isDisabled) {
100
+ return
101
+ }
102
+ const next = String(nextValue).replace(/\D/g, '').slice(0, length)
103
+ if (!isControlled) {
104
+ setInternalValue(next)
105
+ }
106
+ if (onChange) {
107
+ onChange(next)
108
+ }
109
+ }
110
+
111
+ return (
112
+ <div
113
+ onClick={() => {
114
+ if (!enableInteraction || isDisabled) {
115
+ return
116
+ }
117
+ if (inputRef.current) {
118
+ inputRef.current.focus()
119
+ }
120
+ }}
121
+ style={{
122
+ display: 'flex',
123
+ gap: 8,
124
+ width: 352,
125
+ position: 'relative',
126
+ }}
127
+ >
128
+ {digits.map((digit, index) => (
129
+ <div
130
+ key={`digit-${index}`}
131
+ style={{
132
+ width: 52,
133
+ height: 64,
134
+ borderRadius: 24,
135
+ backgroundColor: colors.background,
136
+ display: 'flex',
137
+ alignItems: 'center',
138
+ justifyContent: 'center',
139
+ padding: 8,
140
+ position: 'relative',
141
+ overflow: 'hidden',
142
+ color: colors.text,
143
+ fontFamily: '"Space Grotesk", "Inter", system-ui, sans-serif',
144
+ fontWeight: 700,
145
+ fontSize: 32,
146
+ lineHeight: 1,
147
+ }}
148
+ >
149
+ {digit}
150
+ {showCaret && index === caretIndex ? (
151
+ <span
152
+ aria-hidden="true"
153
+ style={{
154
+ position: 'absolute',
155
+ bottom: 14,
156
+ width: 10,
157
+ height: 2,
158
+ borderRadius: 2,
159
+ backgroundColor: colors.text,
160
+ }}
161
+ />
162
+ ) : null}
163
+ </div>
164
+ ))}
165
+ <input
166
+ ref={inputRef}
167
+ inputMode="numeric"
168
+ autoComplete="one-time-code"
169
+ pattern="[0-9]*"
170
+ type="text"
171
+ value={sanitizedValue}
172
+ onChange={(event) => handleChange(event.target.value)}
173
+ onFocus={() => {
174
+ if (!enableInteraction || isDisabled) {
175
+ return
176
+ }
177
+ setIsFocused(true)
178
+ }}
179
+ onBlur={() => setIsFocused(false)}
180
+ style={{
181
+ position: 'absolute',
182
+ inset: 0,
183
+ opacity: 0,
184
+ pointerEvents: 'none',
185
+ }}
186
+ aria-label="Digit field"
187
+ disabled={isDisabled}
188
+ />
189
+ </div>
190
+ )
191
+ }