@ecohouse/ui 0.1.59 → 0.1.61

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 (41) hide show
  1. package/dist/index.cjs +665 -468
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +33 -12
  4. package/dist/index.d.ts +33 -12
  5. package/dist/index.js +558 -366
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/components/Button/Button.tsx +6 -2
  9. package/src/components/CommentCard/CommentCard.tsx +3 -0
  10. package/src/components/Counter/Counter.tsx +5 -3
  11. package/src/components/DatePicker/DatePicker.tsx +98 -40
  12. package/src/components/Range/Range.tsx +1 -1
  13. package/src/components/VerificationCodeInput/VerificationCodeInput.tsx +28 -4
  14. package/src/icons/Backspace/BackspaceIcon.test.ts +9 -0
  15. package/src/icons/Backspace/BackspaceIcon.tsx +20 -0
  16. package/src/icons/Backspace/BackspaceIcon.web.tsx +26 -0
  17. package/src/icons/Backspace/backspacePath.ts +3 -0
  18. package/src/icons/Backspace/index.ts +1 -0
  19. package/src/icons/Fingerprint/FingerprintIcon.test.ts +9 -0
  20. package/src/icons/Fingerprint/FingerprintIcon.tsx +23 -0
  21. package/src/icons/Fingerprint/FingerprintIcon.web.tsx +29 -0
  22. package/src/icons/Fingerprint/fingerprintPaths.ts +6 -0
  23. package/src/icons/Fingerprint/index.ts +1 -0
  24. package/src/icons/GoogleBrand/GoogleBrandIcon.test.ts +14 -0
  25. package/src/icons/GoogleBrand/GoogleBrandIcon.tsx +15 -0
  26. package/src/icons/GoogleBrand/GoogleBrandIcon.web.tsx +21 -0
  27. package/src/icons/GoogleBrand/googleBrandPaths.ts +18 -0
  28. package/src/icons/GoogleBrand/index.ts +1 -0
  29. package/src/icons/PaymentCard/PaymentCardIcon.tsx +14 -0
  30. package/src/icons/PaymentCard/PaymentCardIcon.web.tsx +13 -0
  31. package/src/icons/PaymentCard/index.ts +1 -0
  32. package/src/icons/PaymentCard/paymentCardPath.ts +3 -0
  33. package/src/icons/Receipt/ReceiptIcon.tsx +14 -0
  34. package/src/icons/Receipt/ReceiptIcon.web.tsx +13 -0
  35. package/src/icons/Receipt/index.ts +1 -0
  36. package/src/icons/Receipt/receiptPath.ts +3 -0
  37. package/src/icons/index.ts +5 -0
  38. package/src/icons/registry.ts +13 -1
  39. package/src/index.ts +5 -0
  40. package/src/theme/typography.ts +8 -8
  41. package/src/utils/dateUtils.ts +7 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecohouse/ui",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Cross-platform presentation-only UI components for EcoHouse (React Native + React Native Web)",
@@ -259,9 +259,13 @@ const styles = StyleSheet.create({
259
259
  contentWithoutIcon: {
260
260
  justifyContent: "center",
261
261
  },
262
- label: {},
262
+ label: {
263
+ flexShrink: 0,
264
+ overflow: "visible",
265
+ paddingRight: 4,
266
+ },
263
267
  labelAndroid: {
264
- includeFontPadding: false,
268
+ includeFontPadding: true,
265
269
  },
266
270
  labelCentered: {
267
271
  textAlign: "center",
@@ -29,6 +29,7 @@ export interface CommentCardProps extends Omit<ViewProps, "style" | "children">
29
29
  onExpandedChange?: (expanded: boolean) => void;
30
30
  style?: StyleProp<ViewStyle>;
31
31
  commentStyle?: StyleProp<TextStyle>;
32
+ actionStyle?: StyleProp<TextStyle>;
32
33
  className?: string;
33
34
  }
34
35
 
@@ -51,6 +52,7 @@ export const CommentCard = forwardRef<ComponentRef<typeof View>, CommentCardProp
51
52
  onExpandedChange,
52
53
  style,
53
54
  commentStyle,
55
+ actionStyle,
54
56
  className,
55
57
  ...rest
56
58
  },
@@ -128,6 +130,7 @@ export const CommentCard = forwardRef<ComponentRef<typeof View>, CommentCardProp
128
130
  <Text
129
131
  style={[
130
132
  styles.actionText,
133
+ actionStyle,
131
134
  expanded && styles.closeActionText,
132
135
  actionHovered &&
133
136
  (expanded ? styles.closeActionTextHovered : styles.openActionTextHovered),
@@ -91,7 +91,7 @@ export const Counter = forwardRef<ComponentRef<typeof View>, CounterProps>(funct
91
91
  accessibilityState={{ disabled: decrementDisabled }}
92
92
  style={[styles.button, webBlurStyle, decrementDisabled && styles.buttonDisabled]}
93
93
  >
94
- <MinusIcon />
94
+ <MinusIcon color={colors.white} strokeWidth={2} />
95
95
  </Pressable>
96
96
  );
97
97
  const increment = (
@@ -104,7 +104,7 @@ export const Counter = forwardRef<ComponentRef<typeof View>, CounterProps>(funct
104
104
  accessibilityState={{ disabled: incrementDisabled }}
105
105
  style={[styles.button, webBlurStyle, incrementDisabled && styles.buttonDisabled]}
106
106
  >
107
- <PlusIcon />
107
+ <PlusIcon color={colors.white} strokeWidth={2} />
108
108
  </Pressable>
109
109
  );
110
110
  const valueNode = (
@@ -141,7 +141,7 @@ const styles = StyleSheet.create({
141
141
  flexDirection: "row",
142
142
  alignItems: "center",
143
143
  alignSelf: "flex-start",
144
- gap: 17,
144
+ gap: 16,
145
145
  },
146
146
  valueFirst: {
147
147
  alignSelf: "stretch",
@@ -153,9 +153,11 @@ const styles = StyleSheet.create({
153
153
  height: BUTTON_SIZE,
154
154
  alignItems: "center",
155
155
  justifyContent: "center",
156
+ padding: 8,
156
157
  borderWidth: 1,
157
158
  borderColor: colors.whiteAlpha10,
158
159
  borderRadius: 39,
160
+ opacity: 1,
159
161
  },
160
162
  buttonDisabled: {
161
163
  opacity: 0.25,
@@ -75,6 +75,8 @@ const EMPTY_RANGE: DateRange = { start: null, end: null };
75
75
 
76
76
  type DatePickerBaseProps = Omit<ViewProps, "style" | "children"> & {
77
77
  placeholder?: string;
78
+ /** Color for the trigger label when no date is selected. Defaults to grey100. */
79
+ placeholderTextColor?: string;
78
80
  /** Trigger presentation. Range variants display separate start/end fields. */
79
81
  variant?: DatePickerVariant;
80
82
  /** Labels shown above the start/end values in range trigger variants. */
@@ -116,7 +118,10 @@ type DatePickerBaseProps = Omit<ViewProps, "style" | "children"> & {
116
118
  formatValue?: (date: Date) => string;
117
119
  /** First day of the week column (0 = Sunday, 1 = Monday). Defaults to Monday. */
118
120
  weekStartsOn?: 0 | 1;
119
- /** Styles the calendar popup independently from the trigger width. */
121
+ /**
122
+ * Styles the calendar popup independently from the trigger width.
123
+ * With `hideTrigger`, `width: "100%"` stretches the day grid to the host.
124
+ */
120
125
  calendarStyle?: StyleProp<ViewStyle>;
121
126
  /**
122
127
  * When true, only the calendar panel is rendered (no field/trigger).
@@ -144,8 +149,7 @@ export type DatePickerRangeProps = DatePickerBaseProps & {
144
149
  export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
145
150
 
146
151
  const DEFAULT_WIDTH = 320;
147
- const DEFAULT_MENU_HEIGHT = 388;
148
- const MENU_CONTENT_WIDTH = 296;
152
+ const MENU_CONTENT_WIDTH = 308;
149
153
  const RANGE_TRIGGER_WIDTH = 382;
150
154
  const TRIGGER_HEIGHT = 44;
151
155
  const TRIGGER_RADIUS = 60;
@@ -157,10 +161,10 @@ const HEADER_HEIGHT = 36;
157
161
  const WEEKDAY_ROW_HEIGHT = 44;
158
162
  const WEEKDAY_ROW_GAP = 8;
159
163
  const WEEK_ROW_GAP = 16;
160
- const DAY_CELL_WIDTH = 42.28571701049805;
164
+ const DAY_CELL_WIDTH = 44;
161
165
  const DAY_CELL_HEIGHT = 44;
162
- const DAY_CELL_RADIUS = 67;
163
- const DAY_CELL_PADDING = 12;
166
+ const DAY_CIRCLE_SIZE = 44;
167
+ const DAY_CELL_RADIUS = 22;
164
168
 
165
169
  function RangeTriggerField({
166
170
  label,
@@ -193,6 +197,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
193
197
  function DatePicker(props, forwardedRef) {
194
198
  const {
195
199
  placeholder = "Select date",
200
+ placeholderTextColor = colors.grey100,
196
201
  variant = "default",
197
202
  startLabel = "Start",
198
203
  endLabel = "End",
@@ -457,11 +462,16 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
457
462
  : isOpen
458
463
  ? colors.primaryMuted
459
464
  : colors.grey700;
460
- const triggerTextColor = hasValue ? colors.white : colors.grey100;
465
+ const triggerTextColor = hasValue || isOpen ? colors.white : placeholderTextColor;
466
+ const triggerIconColor = isOpen ? colors.white : colors.grey150;
461
467
  const startValue = rangeValue.start ? formatValue(rangeValue.start) : startPlaceholder;
462
468
  const endValue = rangeValue.end ? formatValue(rangeValue.end) : endPlaceholder;
463
469
 
464
470
  const resolvedAccessibilityLabel = accessibilityLabel ?? placeholder;
471
+ const stretchCalendar =
472
+ hideTrigger &&
473
+ (StyleSheet.flatten(calendarStyle)?.width === "100%" ||
474
+ StyleSheet.flatten(style)?.width === "100%");
465
475
 
466
476
  return (
467
477
  <View
@@ -471,13 +481,20 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
471
481
  styles.root,
472
482
  usesRangeTrigger && styles.rangeRoot,
473
483
  hideTrigger && styles.rootHideTrigger,
484
+ stretchCalendar && styles.rootFullWidth,
474
485
  isOpen && styles.rootOpen,
475
486
  disabled && styles.disabled,
476
487
  style,
477
488
  ]}
478
489
  accessibilityState={{ disabled, expanded: isOpen }}
479
490
  >
480
- <View style={[styles.triggerWrap, isOpen && styles.triggerWrapOpen]}>
491
+ <View
492
+ style={[
493
+ styles.triggerWrap,
494
+ isOpen && styles.triggerWrapOpen,
495
+ stretchCalendar && styles.triggerWrapFullWidth,
496
+ ]}
497
+ >
481
498
  {hideTrigger ? null : (
482
499
  <Pressable
483
500
  ref={triggerRef}
@@ -517,7 +534,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
517
534
  {triggerLabel}
518
535
  </Text>
519
536
  <View style={styles.iconSlot}>
520
- <CalendarOutlineIcon size={ICON_SIZE} color={colors.grey100} />
537
+ <CalendarOutlineIcon size={ICON_SIZE} color={triggerIconColor} />
521
538
  </View>
522
539
  </>
523
540
  )}
@@ -531,14 +548,17 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
531
548
  usesRangeTrigger && styles.rangeMenu,
532
549
  hideTrigger
533
550
  ? styles.menuInline
534
- : menuAlignEnd
551
+ : Platform.OS !== "web" || menuAlignEnd
535
552
  ? styles.menuAlignEnd
536
553
  : styles.menuAlignStart,
537
- calendarStyle,
538
554
  styles.menuFitContent,
555
+ stretchCalendar && styles.menuFullWidth,
556
+ calendarStyle,
539
557
  ]}
540
558
  >
541
- <View style={styles.calendarHeader}>
559
+ <View
560
+ style={[styles.calendarHeader, stretchCalendar && styles.calendarSectionFullWidth]}
561
+ >
542
562
  <Pressable
543
563
  onPress={handlePrevMonth}
544
564
  hitSlop={8}
@@ -566,10 +586,17 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
566
586
  </Pressable>
567
587
  </View>
568
588
 
569
- <View style={styles.calendarGrid}>
570
- <View style={styles.weekdayRow}>
589
+ <View
590
+ style={[styles.calendarGrid, stretchCalendar && styles.calendarSectionFullWidth]}
591
+ >
592
+ <View
593
+ style={[styles.weekdayRow, stretchCalendar && styles.calendarSectionFullWidth]}
594
+ >
571
595
  {weekdayLabels.map((weekdayLabel, index) => (
572
- <View key={`${weekdayLabel}-${index}`} style={styles.weekdayCell}>
596
+ <View
597
+ key={`${weekdayLabel}-${index}`}
598
+ style={[styles.weekdayCell, stretchCalendar && styles.dayCellFullWidth]}
599
+ >
573
600
  <Text style={styles.weekdayText}>{weekdayLabel}</Text>
574
601
  </View>
575
602
  ))}
@@ -590,21 +617,9 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
590
617
  if (rangeIndices.length > 0) {
591
618
  const first = rangeIndices[0];
592
619
  const last = rangeIndices[rangeIndices.length - 1];
593
- const startsHere = isSameDay(week[first], rangeValue.start);
594
- const endsHere = isSameDay(week[last], rangeValue.end);
595
- // Tuck past the circle center so the highlight's rounded cap
596
- // sits under the endpoint circle instead of leaving a gap.
597
620
  const cellPct = 100 / 7;
598
- const halfCellPct = cellPct / 2;
599
- const tuckPct = (12 / MENU_CONTENT_WIDTH) * 100;
600
- let leftPct = first * cellPct;
601
- let rightPct = (last + 1) * cellPct;
602
- if (startsHere) {
603
- leftPct += halfCellPct - tuckPct;
604
- }
605
- if (endsHere) {
606
- rightPct -= halfCellPct - tuckPct;
607
- }
621
+ const leftPct = first * cellPct;
622
+ const rightPct = (last + 1) * cellPct;
608
623
  const widthPct = rightPct - leftPct;
609
624
  // Same-day range: only the circle, no connector bar.
610
625
  if (widthPct > 0) {
@@ -619,7 +634,11 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
619
634
  return (
620
635
  <View
621
636
  key={weekIndex}
622
- style={[styles.weekRow, weekIndex > 0 && styles.weekRowSpaced]}
637
+ style={[
638
+ styles.weekRow,
639
+ weekIndex > 0 && styles.weekRowSpaced,
640
+ stretchCalendar && styles.calendarSectionFullWidth,
641
+ ]}
623
642
  >
624
643
  {rangeHighlightStyle ? (
625
644
  <View
@@ -658,6 +677,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
658
677
  return (
659
678
  <Pressable
660
679
  key={dayIndex}
680
+ android_ripple={{ borderless: true, color: "transparent", radius: 22 }}
661
681
  onPress={() => handleDayPress(day)}
662
682
  disabled={dayDisabled || dayBlocked}
663
683
  onHoverIn={() => setHoveredDayKey(dayKey)}
@@ -668,7 +688,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
668
688
  onPressOut={() =>
669
689
  setHoveredDayKey((current) => (current === dayKey ? null : current))
670
690
  }
671
- style={styles.dayCellWrap}
691
+ style={[styles.dayCellWrap, stretchCalendar && styles.dayCellFullWidth]}
672
692
  accessibilityRole="button"
673
693
  accessibilityLabel={
674
694
  dayUnavailable
@@ -726,6 +746,10 @@ const styles = StyleSheet.create({
726
746
  width: "auto",
727
747
  gap: 0,
728
748
  },
749
+ rootFullWidth: {
750
+ width: "100%",
751
+ alignSelf: "stretch",
752
+ },
729
753
  rootOpen: {
730
754
  zIndex: 30,
731
755
  },
@@ -742,6 +766,10 @@ const styles = StyleSheet.create({
742
766
  triggerWrapOpen: {
743
767
  zIndex: 20,
744
768
  },
769
+ triggerWrapFullWidth: {
770
+ width: "100%",
771
+ alignSelf: "stretch",
772
+ },
745
773
  trigger: {
746
774
  height: TRIGGER_HEIGHT,
747
775
  borderRadius: TRIGGER_RADIUS,
@@ -832,7 +860,7 @@ const styles = StyleSheet.create({
832
860
  position: "absolute",
833
861
  top: "100%",
834
862
  alignSelf: "flex-start",
835
- height: DEFAULT_MENU_HEIGHT,
863
+ height: "auto",
836
864
  marginTop: 8,
837
865
  zIndex: 10,
838
866
  borderRadius: MENU_RADIUS,
@@ -840,7 +868,7 @@ const styles = StyleSheet.create({
840
868
  gap: 0,
841
869
  opacity: 1,
842
870
  backgroundColor: colors.grey700,
843
- overflow: "hidden",
871
+ overflow: "visible",
844
872
  ...Platform.select({
845
873
  web: {
846
874
  boxShadow: `0 8px 24px ${colors.black}73`,
@@ -855,6 +883,7 @@ const styles = StyleSheet.create({
855
883
  right: "auto",
856
884
  },
857
885
  menuAlignEnd: {
886
+ alignSelf: "flex-end",
858
887
  left: "auto",
859
888
  right: 0,
860
889
  },
@@ -865,16 +894,34 @@ const styles = StyleSheet.create({
865
894
  right: "auto",
866
895
  marginTop: 0,
867
896
  },
897
+ menuFullWidth: {
898
+ width: "100%",
899
+ maxWidth: "100%",
900
+ alignSelf: "stretch",
901
+ alignItems: "stretch",
902
+ },
903
+ calendarSectionFullWidth: {
904
+ width: "100%",
905
+ alignSelf: "stretch",
906
+ },
907
+ dayCellFullWidth: {
908
+ width: 0,
909
+ flexGrow: 1,
910
+ flexShrink: 1,
911
+ minWidth: 0,
912
+ },
868
913
  rangeMenu: {},
869
914
  menuFitContent: {
870
915
  width: MENU_CONTENT_WIDTH + MENU_PADDING * 2,
871
916
  maxWidth: MENU_CONTENT_WIDTH + MENU_PADDING * 2,
917
+ height: "auto",
872
918
  alignSelf: "flex-start",
873
919
  alignItems: "flex-start",
874
920
  ...(Platform.select({
875
921
  web: {
876
922
  width: "fit-content",
877
923
  maxWidth: "fit-content",
924
+ height: "fit-content",
878
925
  },
879
926
  default: {},
880
927
  }) as ViewStyle),
@@ -882,16 +929,18 @@ const styles = StyleSheet.create({
882
929
  calendarGrid: {
883
930
  width: MENU_CONTENT_WIDTH,
884
931
  alignSelf: "flex-start",
932
+ overflow: "visible",
885
933
  },
886
934
  calendarHeader: {
887
935
  width: MENU_CONTENT_WIDTH,
888
- height: HEADER_HEIGHT,
936
+ minHeight: HEADER_HEIGHT,
889
937
  paddingTop: 8,
890
938
  paddingBottom: 8,
891
939
  flexDirection: "row",
892
940
  alignItems: "center",
893
941
  justifyContent: "space-between",
894
942
  opacity: 1,
943
+ overflow: "visible",
895
944
  },
896
945
  navButton: {
897
946
  width: HEADER_HEIGHT,
@@ -905,6 +954,9 @@ const styles = StyleSheet.create({
905
954
  monthHeading: {
906
955
  ...datePickerTypography.monthHeading,
907
956
  color: colors.white,
957
+ flexShrink: 0,
958
+ includeFontPadding: true,
959
+ overflow: "visible",
908
960
  textAlign: "center",
909
961
  },
910
962
  weekdayRow: {
@@ -931,7 +983,9 @@ const styles = StyleSheet.create({
931
983
  },
932
984
  weekRow: {
933
985
  width: MENU_CONTENT_WIDTH,
986
+ height: DAY_CELL_HEIGHT,
934
987
  flexDirection: "row",
988
+ overflow: "visible",
935
989
  position: "relative",
936
990
  },
937
991
  weekRowSpaced: {
@@ -940,7 +994,7 @@ const styles = StyleSheet.create({
940
994
  rangeHighlight: {
941
995
  position: "absolute",
942
996
  top: 0,
943
- bottom: 0,
997
+ height: DAY_CELL_HEIGHT,
944
998
  borderRadius: DAY_CELL_RADIUS,
945
999
  backgroundColor: colors.primaryHover,
946
1000
  },
@@ -952,21 +1006,25 @@ const styles = StyleSheet.create({
952
1006
  alignItems: "center",
953
1007
  justifyContent: "center",
954
1008
  opacity: 1,
1009
+ overflow: "visible",
955
1010
  },
956
1011
  dayCircle: {
957
- width: "100%",
958
- height: "100%",
959
- borderRadius: DAY_CELL_RADIUS,
960
- padding: DAY_CELL_PADDING,
961
- gap: 10,
1012
+ width: DAY_CIRCLE_SIZE,
1013
+ height: DAY_CIRCLE_SIZE,
1014
+ borderRadius: 22,
1015
+ overflow: "hidden",
962
1016
  alignItems: "center",
963
1017
  justifyContent: "center",
964
1018
  },
965
1019
  dayCircleSelected: {
966
1020
  backgroundColor: colors.primary,
1021
+ borderRadius: 22,
1022
+ overflow: "hidden",
967
1023
  },
968
1024
  dayCircleBooked: {
969
1025
  backgroundColor: colors.grey700,
1026
+ borderRadius: 22,
1027
+ overflow: "hidden",
970
1028
  },
971
1029
  dayTextBooked: {
972
1030
  textDecorationLine: "line-through",
@@ -35,7 +35,7 @@ import {
35
35
 
36
36
  const DEFAULT_WIDTH = 347;
37
37
  const TRACK_HEIGHT = 4;
38
- const TRACK_HORIZONTAL_INSET = 20;
38
+ const TRACK_HORIZONTAL_INSET = 0;
39
39
  const THUMB_SIZE = 20;
40
40
  const INPUT_WIDTH = 120;
41
41
 
@@ -28,7 +28,7 @@ const DEFAULT_LENGTH = 6;
28
28
  const DESKTOP_CELL_WIDTH = 54.333335876464844;
29
29
  const MOBILE_CELL_WIDTH = 45.16666793823242;
30
30
  const DESKTOP_CELL_HEIGHT = 68;
31
- const MOBILE_CELL_HEIGHT = 58;
31
+ const MOBILE_CELL_HEIGHT = 68;
32
32
  const DESKTOP_GAP = 12;
33
33
  const MOBILE_GAP = 12;
34
34
  const MOBILE_BREAKPOINT = 1024;
@@ -172,8 +172,18 @@ export const VerificationCodeInput = forwardRef<
172
172
  const showErrorMessage = Boolean(error && errorMessage);
173
173
 
174
174
  return (
175
- <View {...viewProps} ref={rootRef} style={[styles.wrapper, disabled && styles.disabled, style]}>
176
- <View style={[styles.row, { width: rowWidth, maxWidth: "100%", gap: cellGap }]}>
175
+ <View
176
+ {...viewProps}
177
+ ref={rootRef}
178
+ style={[styles.wrapper, disabled && styles.disabled, mobile && styles.wrapperFill, style]}
179
+ >
180
+ <View
181
+ style={[
182
+ styles.row,
183
+ mobile ? styles.rowFill : { width: rowWidth, maxWidth: "100%" },
184
+ { gap: cellGap },
185
+ ]}
186
+ >
177
187
  {Array.from({ length: safeLength }, (_, index) => {
178
188
  const active = focusedIndex === index;
179
189
  const borderColor = error ? colors.red : active ? colors.primary : colors.grey600;
@@ -203,8 +213,8 @@ export const VerificationCodeInput = forwardRef<
203
213
  selectTextOnFocus
204
214
  style={[
205
215
  styles.cell,
216
+ mobile ? styles.cellFill : { width: cellWidth },
206
217
  {
207
- width: cellWidth,
208
218
  height: cellHeight,
209
219
  borderColor,
210
220
  color: error ? colors.red : colors.white,
@@ -235,11 +245,25 @@ const styles = StyleSheet.create({
235
245
  alignItems: "center",
236
246
  gap: 12,
237
247
  },
248
+ wrapperFill: {
249
+ alignSelf: "stretch",
250
+ width: "100%",
251
+ },
238
252
  row: {
239
253
  minWidth: 0,
240
254
  flexDirection: "row",
241
255
  justifyContent: "center",
242
256
  },
257
+ rowFill: {
258
+ maxWidth: "100%",
259
+ width: "100%",
260
+ },
261
+ cellFill: {
262
+ flexBasis: 0,
263
+ flexGrow: 1,
264
+ flexShrink: 1,
265
+ width: undefined,
266
+ },
243
267
  disabled: {
244
268
  opacity: 0.6,
245
269
  },
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { BACKSPACE_PATH } from "./backspacePath";
3
+
4
+ describe("BackspaceIcon", () => {
5
+ it("uses the approved 20px path", () => {
6
+ expect(BACKSPACE_PATH).toContain("M14.1667 7.49992");
7
+ expect(BACKSPACE_PATH).toMatch(/2\.26667 10\.7999Z$/);
8
+ });
9
+ });
@@ -0,0 +1,20 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+ import { colors } from "../../theme";
3
+ import type { IconProps } from "../types";
4
+ import { BACKSPACE_PATH } from "./backspacePath";
5
+
6
+ export { BACKSPACE_PATH } from "./backspacePath";
7
+
8
+ export function BackspaceIcon({ size = 20, color = colors.white, strokeWidth = 2 }: IconProps) {
9
+ return (
10
+ <Svg width={size} height={size} viewBox="0 0 20 20" fill="none">
11
+ <Path
12
+ d={BACKSPACE_PATH}
13
+ stroke={color}
14
+ strokeWidth={strokeWidth}
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ />
18
+ </Svg>
19
+ );
20
+ }
@@ -0,0 +1,26 @@
1
+ import { colors } from "../../theme";
2
+ import type { IconProps } from "../types";
3
+ import { BACKSPACE_PATH } from "./backspacePath";
4
+
5
+ export function BackspaceIcon({ size = 20, color = colors.white, strokeWidth = 2 }: IconProps) {
6
+ return (
7
+ <svg
8
+ width={size}
9
+ height={size}
10
+ viewBox="0 0 20 20"
11
+ fill="none"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ aria-hidden
14
+ >
15
+ <path
16
+ d={BACKSPACE_PATH}
17
+ stroke={color}
18
+ strokeWidth={strokeWidth}
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ />
22
+ </svg>
23
+ );
24
+ }
25
+
26
+ export { BACKSPACE_PATH } from "./backspacePath";
@@ -0,0 +1,3 @@
1
+ /** Backspace / delete path — 20×20 viewBox. */
2
+ export const BACKSPACE_PATH =
3
+ "M14.1667 7.49992L9.16667 12.4999M9.16667 7.49992L14.1667 12.4999M2.26667 10.7999L5.86667 15.5999C6.16 15.991 6.30667 16.1866 6.49254 16.3276C6.65717 16.4525 6.8436 16.5458 7.04231 16.6025C7.26667 16.6666 7.51111 16.6666 8 16.6666H14.3333C15.7335 16.6666 16.4335 16.6666 16.9683 16.3941C17.4387 16.1544 17.8212 15.772 18.0609 15.3016C18.3333 14.7668 18.3333 14.0667 18.3333 12.6666V7.33325C18.3333 5.93312 18.3333 5.23306 18.0609 4.69828C17.8212 4.22787 17.4387 3.84542 16.9683 3.60574C16.4335 3.33325 15.7335 3.33325 14.3333 3.33325H8C7.51111 3.33325 7.26667 3.33325 7.04231 3.39733C6.8436 3.45408 6.65717 3.54729 6.49254 3.67221C6.30667 3.81325 6.16 4.00881 5.86667 4.39992L2.26667 9.19992C2.05151 9.4868 1.94392 9.63024 1.90245 9.78778C1.86585 9.92684 1.86585 10.073 1.90245 10.2121C1.94392 10.3696 2.05151 10.513 2.26667 10.7999Z";
@@ -0,0 +1 @@
1
+ export { BackspaceIcon, BACKSPACE_PATH } from "./BackspaceIcon";
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { FINGERPRINT_PATHS } from "./fingerprintPaths";
3
+
4
+ describe("FingerprintIcon", () => {
5
+ it("uses the approved 20px paths", () => {
6
+ expect(FINGERPRINT_PATHS).toHaveLength(4);
7
+ expect(FINGERPRINT_PATHS[0]).toContain("M6.75365 17.6842");
8
+ });
9
+ });
@@ -0,0 +1,23 @@
1
+ import Svg, { Path } from "react-native-svg";
2
+ import { colors } from "../../theme";
3
+ import type { IconProps } from "../types";
4
+ import { FINGERPRINT_PATHS } from "./fingerprintPaths";
5
+
6
+ export { FINGERPRINT_PATHS } from "./fingerprintPaths";
7
+
8
+ export function FingerprintIcon({ size = 20, color = colors.white, strokeWidth = 2 }: IconProps) {
9
+ return (
10
+ <Svg width={size} height={size} viewBox="0 0 20 20" fill="none">
11
+ {FINGERPRINT_PATHS.map((d) => (
12
+ <Path
13
+ key={d}
14
+ d={d}
15
+ stroke={color}
16
+ strokeWidth={strokeWidth}
17
+ strokeLinecap="round"
18
+ strokeLinejoin="round"
19
+ />
20
+ ))}
21
+ </Svg>
22
+ );
23
+ }
@@ -0,0 +1,29 @@
1
+ import { colors } from "../../theme";
2
+ import type { IconProps } from "../types";
3
+ import { FINGERPRINT_PATHS } from "./fingerprintPaths";
4
+
5
+ export function FingerprintIcon({ size = 20, color = colors.white, strokeWidth = 2 }: IconProps) {
6
+ return (
7
+ <svg
8
+ width={size}
9
+ height={size}
10
+ viewBox="0 0 20 20"
11
+ fill="none"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ aria-hidden
14
+ >
15
+ {FINGERPRINT_PATHS.map((d) => (
16
+ <path
17
+ key={d}
18
+ d={d}
19
+ stroke={color}
20
+ strokeWidth={strokeWidth}
21
+ strokeLinecap="round"
22
+ strokeLinejoin="round"
23
+ />
24
+ ))}
25
+ </svg>
26
+ );
27
+ }
28
+
29
+ export { FINGERPRINT_PATHS } from "./fingerprintPaths";
@@ -0,0 +1,6 @@
1
+ export const FINGERPRINT_PATHS = [
2
+ "M6.75365 17.6842C5.59542 16.0313 4.91406 14.0203 4.91406 11.8489C4.91406 9.03693 7.19233 6.75781 10.0026 6.75781C12.8129 6.75781 15.0912 9.03693 15.0912 11.8489",
3
+ "M15.3626 16.9267C15.271 16.9317 15.1811 16.9402 15.0886 16.9402C12.2783 16.9402 10 14.6611 10 11.8491",
4
+ "M11.0569 18.3334C8.89484 16.9851 7.4549 14.5855 7.4549 11.8491C7.4549 10.4431 8.59446 9.30351 9.99961 9.30351C11.4048 9.30351 12.5443 10.4431 12.5443 11.8491C12.5443 13.2551 13.6839 14.3946 15.089 14.3946C16.4942 14.3946 17.6337 13.2551 17.6337 11.8491C17.6337 7.63108 14.2159 4.2124 10.0005 4.2124C5.78502 4.2124 2.36719 7.63108 2.36719 11.8491C2.36719 12.7892 2.4724 13.7048 2.66587 14.5872",
5
+ "M17.1077 4.88578C15.3725 2.91467 12.8354 1.6665 10.0031 1.6665C7.17073 1.6665 4.63366 2.91467 2.89844 4.88578",
6
+ ] as const;
@@ -0,0 +1 @@
1
+ export { FingerprintIcon, FINGERPRINT_PATHS } from "./FingerprintIcon";
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { GOOGLE_BRAND_PATHS } from "./googleBrandPaths";
3
+
4
+ describe("GoogleBrandIcon", () => {
5
+ it("uses the approved brand-colored paths", () => {
6
+ expect(GOOGLE_BRAND_PATHS).toHaveLength(4);
7
+ expect(GOOGLE_BRAND_PATHS.map((path) => path.fill)).toEqual([
8
+ "#4285F4",
9
+ "#34A853",
10
+ "#FBBC05",
11
+ "#A63E3E",
12
+ ]);
13
+ });
14
+ });