@ecohouse/ui 0.1.62 → 0.1.64

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecohouse/ui",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Cross-platform presentation-only UI components for EcoHouse (React Native + React Native Web)",
@@ -87,6 +87,17 @@ const triggerWebBlurStyle: ViewStyle =
87
87
  } as ViewStyle)
88
88
  : {};
89
89
 
90
+ // Fades the light/dark switch (e.g. a header changing variant on scroll) instead of snapping.
91
+ const variantWebTransitionStyle: ViewStyle =
92
+ Platform.OS === "web"
93
+ ? ({
94
+ transitionProperty:
95
+ "background-color, border-color, box-shadow, backdrop-filter, -webkit-backdrop-filter",
96
+ transitionDuration: "200ms",
97
+ transitionTimingFunction: "ease",
98
+ } as ViewStyle)
99
+ : {};
100
+
90
101
  const menuWebBlurStyle: ViewStyle =
91
102
  Platform.OS === "web"
92
103
  ? ({
@@ -219,6 +230,7 @@ export const Avatar = forwardRef<ComponentRef<typeof View>, AvatarProps>(functio
219
230
  }
220
231
  style={[
221
232
  styles.root,
233
+ variantWebTransitionStyle,
222
234
  isLightVariant ? styles.rootLight : styles.rootDark,
223
235
  isLightVariant && triggerWebBlurStyle,
224
236
  disabled && styles.disabled,
@@ -275,6 +287,7 @@ export const Avatar = forwardRef<ComponentRef<typeof View>, AvatarProps>(functio
275
287
  <View
276
288
  style={[
277
289
  styles.menu,
290
+ variantWebTransitionStyle,
278
291
  isLightVariant ? styles.menuLight : styles.menuDark,
279
292
  isLightVariant && menuWebBlurStyle,
280
293
  menuWidth != null && { right: undefined, width: menuWidth },
@@ -292,13 +305,8 @@ export const Avatar = forwardRef<ComponentRef<typeof View>, AvatarProps>(functio
292
305
  hoverColor={item.hoverColor ?? (isLightVariant ? colors.whiteAlpha7 : undefined)}
293
306
  labelStyle={item.labelStyle}
294
307
  onPress={() => handleItemPress(item)}
295
- style={
296
- isLightVariant
297
- ? item.backgroundColor
298
- ? styles.emphasizedMenuItem
299
- : styles.menuItem
300
- : undefined
301
- }
308
+ // Same size in both variants so the menu keeps its height when the variant changes.
309
+ style={item.backgroundColor ? styles.emphasizedMenuItem : styles.menuItem}
302
310
  />
303
311
  );
304
312
  })}
@@ -409,6 +417,9 @@ const styles = StyleSheet.create({
409
417
  overflow: "hidden",
410
418
  },
411
419
  menuDark: {
420
+ // Matches menuLight's border so switching variants does not shift the menu by 2px.
421
+ borderWidth: 1,
422
+ borderColor: "transparent",
412
423
  backgroundColor: colors.grey700,
413
424
  ...Platform.select({
414
425
  web: {
@@ -76,6 +76,18 @@ const bookingRange = {
76
76
  const formatBookingDate = (date: Date) =>
77
77
  `${String(date.getDate()).padStart(2, "0")}/${String(date.getMonth() + 1).padStart(2, "0")}/${date.getFullYear()}`;
78
78
 
79
+ /** Unavailable days: dimmed number on a filled circle, not selectable. */
80
+ export const RangeWithUnavailableDates: Story = {
81
+ args: {
82
+ range: true,
83
+ placeholder: "Select dates",
84
+ defaultValue: { start: new Date(2026, 5, 15), end: new Date(2026, 5, 24) },
85
+ unavailableDates: [new Date(2026, 5, 8), new Date(2026, 5, 9)],
86
+ blockUnavailableDates: true,
87
+ defaultOpen: true,
88
+ },
89
+ };
90
+
79
91
  export const RangeHorizontal: Story = {
80
92
  args: {
81
93
  range: true,
@@ -695,7 +695,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
695
695
  style={[
696
696
  styles.dayCircle,
697
697
  isSelectedEndpoint && styles.dayCircleSelected,
698
- dayUnavailable && styles.dayCircleBooked,
698
+ dayUnavailable && styles.dayCircleUnavailable,
699
699
  isHovered && styles.dayCircleHovered,
700
700
  ]}
701
701
  >
@@ -703,7 +703,7 @@ export const DatePicker = forwardRef<ComponentRef<typeof View>, DatePickerProps>
703
703
  style={[
704
704
  styles.dayText,
705
705
  { color: dayTextColor },
706
- dayUnavailable && styles.dayTextBooked,
706
+ dayUnavailable && styles.dayTextUnavailable,
707
707
  ]}
708
708
  >
709
709
  {day.getDate()}
@@ -1012,13 +1012,14 @@ const styles = StyleSheet.create({
1012
1012
  borderRadius: 22,
1013
1013
  overflow: "hidden",
1014
1014
  },
1015
- dayCircleBooked: {
1016
- backgroundColor: colors.grey700,
1015
+ /** Unavailable day: filled circle a step lighter than the calendar, dimmed number. */
1016
+ dayCircleUnavailable: {
1017
+ backgroundColor: colors.grey600,
1017
1018
  borderRadius: 22,
1018
1019
  overflow: "hidden",
1019
1020
  },
1020
- dayTextBooked: {
1021
- textDecorationLine: "line-through",
1021
+ dayTextUnavailable: {
1022
+ color: colors.grey200,
1022
1023
  },
1023
1024
  dayCircleHovered: {
1024
1025
  backgroundColor: colors.grey600,
@@ -32,6 +32,20 @@ describe("updateVerificationCells", () => {
32
32
  });
33
33
  });
34
34
 
35
+ it("lays a whole pasted code out from the start when a later cell has focus", () => {
36
+ expect(updateVerificationCells(toVerificationCells("", 6), 4, "123456", 6)).toEqual({
37
+ cells: ["1", "2", "3", "4", "5", "6"],
38
+ nextFocus: 5,
39
+ });
40
+ });
41
+
42
+ it("still fills forward when the pasted digits fit from the focused cell", () => {
43
+ expect(updateVerificationCells(toVerificationCells("12", 6), 2, "3456", 6)).toEqual({
44
+ cells: ["1", "2", "3", "4", "5", "6"],
45
+ nextFocus: 5,
46
+ });
47
+ });
48
+
35
49
  it("replaces a selected digit without growing the code", () => {
36
50
  expect(updateVerificationCells(toVerificationCells("123456", 6), 2, "9", 6)).toEqual({
37
51
  cells: ["1", "2", "9", "4", "5", "6"],
@@ -51,11 +51,16 @@ export function updateVerificationCells(
51
51
  length: number,
52
52
  ): VerificationCellsUpdate {
53
53
  const safeLength = Math.max(1, length);
54
- const safeIndex = Math.min(Math.max(0, index), safeLength - 1);
54
+ const focusedIndex = Math.min(Math.max(0, index), safeLength - 1);
55
55
  const cells = Array.from({ length: safeLength }, (_, cellIndex) => {
56
56
  const cell = currentCells[cellIndex] ?? "";
57
57
  return /^\d$/.test(cell) ? cell : "";
58
58
  });
59
+ // A paste that does not fit from the focused cell (typically a whole code pasted while a
60
+ // later cell has focus) is laid out from the first cell instead of being cut to the tail.
61
+ const pastedDigits = input.replace(/\D/g, "");
62
+ const safeIndex =
63
+ pastedDigits.length > 1 && pastedDigits.length > safeLength - focusedIndex ? 0 : focusedIndex;
59
64
  const insertedDigits = normalizeVerificationCode(input, safeLength - safeIndex);
60
65
 
61
66
  if (!insertedDigits) {