@momo-kits/foundation 0.168.1-beta.2 → 0.168.1-beta.21

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.
@@ -1,16 +1,18 @@
1
+ /* eslint-disable react-hooks/exhaustive-deps */
1
2
  import React, {
2
3
  forwardRef,
3
4
  useContext,
4
5
  useEffect,
5
6
  useImperativeHandle,
6
- useLayoutEffect,
7
7
  useRef,
8
8
  useState,
9
9
  } from 'react';
10
10
  import {
11
+ Platform,
12
+ Pressable,
13
+ Text,
11
14
  TextInput,
12
15
  TextInputFocusEvent,
13
- TextInputSelectionChangeEvent,
14
16
  TouchableOpacity,
15
17
  View,
16
18
  } from 'react-native';
@@ -22,7 +24,6 @@ import {
22
24
  } from '../Context';
23
25
  import { Icon } from '../Icon';
24
26
  import { useScaleSize, exportFontFamily } from '../Text';
25
- import { Spacing } from '../Consts';
26
27
  import {
27
28
  ErrorView,
28
29
  FloatingView,
@@ -34,93 +35,22 @@ import { InputMoneyProps } from './index';
34
35
  import styles from './styles';
35
36
  import SystemTextInput from './SystemTextInput';
36
37
 
37
- type Selection = { start: number; end: number };
38
- type Edit = { start: number; end: number; inserted: string };
38
+ const moneyFormatter = new Intl.NumberFormat('vi-VN');
39
39
 
40
- const countDigits = (text: string) => text.replace(/\D/g, '').length;
41
-
42
- const groupDigits = (digits: string) =>
43
- digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
44
-
45
- const caretFromDigitIndex = (display: string, digitIndex: number) => {
46
- let count = 0;
47
- for (let i = 0; i < display.length && count < digitIndex; i++) {
48
- if (/\d/.test(display[i])) {
49
- count += 1;
50
- if (count === digitIndex) return i + 1;
51
- }
52
- }
53
- return 0;
54
- };
55
-
56
- const detectEdit = (old: string, next: string, selection: Selection): Edit => {
57
- const { start, end } = selection;
58
- const removed = old.length - next.length;
59
- const candidates: Edit[] = [
60
- {
61
- start,
62
- end,
63
- inserted: next.slice(start, next.length - (old.length - end)),
64
- },
65
- { start: start - removed, end: start, inserted: '' },
66
- { start, end: start + removed, inserted: '' },
67
- ];
68
- const match = candidates.find(
69
- e =>
70
- e.start >= 0 &&
71
- e.end >= e.start &&
72
- e.end <= old.length &&
73
- old.slice(0, e.start) + e.inserted + old.slice(e.end) === next,
74
- );
75
- if (match) return match;
76
-
77
- let prefix = 0;
78
- while (
79
- prefix < Math.min(old.length, next.length) &&
80
- old[prefix] === next[prefix]
81
- ) {
82
- prefix += 1;
40
+ const findEdit = (old: string, text: string) => {
41
+ let start = 0;
42
+ while (start < text.length && old[start] === text[start]) {
43
+ start += 1;
83
44
  }
84
45
  let suffix = 0;
85
46
  while (
86
- suffix < Math.min(old.length, next.length) - prefix &&
87
- old[old.length - 1 - suffix] === next[next.length - 1 - suffix]
47
+ suffix < Math.min(text.length, old.length) - start &&
48
+ old[old.length - 1 - suffix] === text[text.length - 1 - suffix]
88
49
  ) {
89
50
  suffix += 1;
90
51
  }
91
- return {
92
- start: prefix,
93
- end: old.length - suffix,
94
- inserted: next.slice(prefix, next.length - suffix),
95
- };
96
- };
97
-
98
- const applyEdit = (display: string, edit: Edit, digitLimit: number) => {
99
- const digits = display.replace(/\D/g, '');
100
- let left = countDigits(display.slice(0, edit.start));
101
- let removed = countDigits(display.slice(edit.start, edit.end));
102
- const inserted = edit.inserted.replace(/\D/g, '');
103
- if (
104
- !removed &&
105
- !inserted &&
106
- left > 0 &&
107
- display.slice(edit.start, edit.end).includes('.')
108
- ) {
109
- left -= 1;
110
- removed = 1;
111
- }
112
- const next = digits.slice(0, left) + inserted + digits.slice(left + removed);
113
- const trimmed = next.replace(/^0+(?=\d)/, '');
114
- if ((!removed && !inserted) || trimmed.length > digitLimit) {
115
- return { digits, caretDigit: left };
116
- }
117
- return {
118
- digits: trimmed,
119
- caretDigit: Math.max(
120
- 0,
121
- left + inserted.length - (next.length - trimmed.length),
122
- ),
123
- };
52
+ const size = old.length + text.length - 2 * (start + suffix);
53
+ return { old, start, suffix, size };
124
54
  };
125
55
 
126
56
  const InputMoney = forwardRef(
@@ -160,14 +90,15 @@ const InputMoney = forwardRef(
160
90
  const inputFontFamily = exportFontFamily('bold');
161
91
  const [focused, setFocused] = useState(false);
162
92
  const [displayValue, setDisplayValue] = useState('');
163
- const [commitCount, setCommitCount] = useState(0);
164
- const displayRef = useRef('');
165
- const digitsRef = useRef('');
166
- const selectionRef = useRef<Selection>({ start: 0, end: 0 });
167
- const pendingCaretRef = useRef<number | null>(null);
168
- const pendingSelectionRef = useRef<Selection | null>(null);
169
- const editingRef = useRef(false);
93
+ const [numericValue, setNumericValue] = useState('');
94
+ const [lastDisplayValue, setLastDisplayValue] = useState('');
170
95
  const inputRef = useRef<TextInput>(null);
96
+ const caretDigitsAfter = useRef<number | undefined>(undefined);
97
+ const lastRawText = useRef('');
98
+ const [caretSelection, setCaretSelection] = useState<{
99
+ start: number;
100
+ end: number;
101
+ }>();
171
102
  const componentName = 'InputMoney';
172
103
 
173
104
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
@@ -177,94 +108,103 @@ const InputMoney = forwardRef(
177
108
  props.accessibilityLabel,
178
109
  );
179
110
 
180
- const digitLimit = props.maxLength ?? Number.POSITIVE_INFINITY;
111
+ const maxLength = (props as any)?.maxLength;
112
+ const hasMaxLength = typeof maxLength === 'number';
113
+ const digitLimit = hasMaxLength ? maxLength : Number.POSITIVE_INFINITY;
181
114
 
182
115
  useEffect(() => {
183
116
  if (value !== undefined && typeof value === 'string') {
184
117
  const cleanValue = value.replace(/\D/g, '').replace(/^0+(?=\d)/, '');
185
- if (cleanValue !== digitsRef.current) {
186
- commit(cleanValue, cleanValue.length);
118
+ if (cleanValue !== numericValue) {
119
+ caretDigitsAfter.current = undefined;
120
+ setNumericValue(cleanValue);
121
+ updateDisplayValue(cleanValue);
187
122
  }
188
123
  }
189
- // eslint-disable-next-line react-hooks/exhaustive-deps
190
124
  }, [value]);
191
125
 
192
- useLayoutEffect(() => {
193
- const caret = pendingCaretRef.current;
194
- if (caret === null || !inputRef.current?.isFocused()) return;
195
- pendingCaretRef.current = null;
196
- inputRef.current.setSelection(caret, caret);
197
- }, [commitCount]);
126
+ useEffect(() => {
127
+ setLastDisplayValue(displayValue);
128
+ }, [displayValue]);
198
129
 
199
- const commit = (digits: string, caretDigit: number) => {
200
- const display = digits ? `${groupDigits(digits)}${currency}` : '';
201
- const caret = caretFromDigitIndex(display, caretDigit);
202
- const changed = digits !== digitsRef.current;
203
- displayRef.current = display;
204
- digitsRef.current = digits;
205
- selectionRef.current = { start: caret, end: caret };
206
- pendingCaretRef.current = caret;
207
- setCommitCount(count => count + 1);
208
- setDisplayValue(display);
209
- return changed;
130
+ useEffect(() => {
131
+ if (!caretSelection) return;
132
+ setCaretSelection(current =>
133
+ current === caretSelection ? undefined : current,
134
+ );
135
+ }, [caretSelection]);
136
+
137
+ const updateDisplayValue = (numeric: string) => {
138
+ const next = numeric ? moneyFormatter.format(Number(numeric)) : '';
139
+ setDisplayValue(next);
140
+
141
+ const digitsAfter = caretDigitsAfter.current;
142
+ caretDigitsAfter.current = undefined;
143
+ if (Platform.OS !== 'android' || digitsAfter === undefined) return;
144
+ let before = parseInputText(next).length - digitsAfter;
145
+ let caret = 0;
146
+ while (before > 0 && caret < next.length) {
147
+ if (/\d/.test(next[caret])) before -= 1;
148
+ caret += 1;
149
+ }
150
+ setCaretSelection({ start: caret, end: caret });
151
+ };
152
+
153
+ const parseInputText = (text: string): string => text.replace(/\D/g, '');
154
+
155
+ const updateNumericValue = (val: string) => {
156
+ let next = val.replace(/[^\d]/g, '').replace(/^0+(?=\d)/, '');
157
+ if (next.length > digitLimit) next = next.slice(0, digitLimit);
158
+ if (/^\d*$/.test(next)) {
159
+ setNumericValue(next);
160
+ updateDisplayValue(next);
161
+ onChangeText?.(next);
162
+ }
210
163
  };
211
164
 
212
165
  const onClearText = () => {
213
- commit('', 0);
166
+ inputRef?.current?.clear();
167
+ inputRef.current?.setNativeProps({ text: '' });
168
+ setDisplayValue('');
169
+ setNumericValue('');
214
170
  onChangeText?.('');
215
171
  };
216
172
 
217
173
  const _onChangeText = (text: string) => {
218
- let digits = '';
219
- let caretDigit = 0;
220
- pendingSelectionRef.current = null;
221
- editingRef.current = true;
222
- setTimeout(() => {
223
- editingRef.current = false;
224
- });
225
- if (text) {
226
- const edit = detectEdit(displayRef.current, text, selectionRef.current);
227
- ({ digits, caretDigit } = applyEdit(
228
- displayRef.current,
229
- edit,
230
- digitLimit,
231
- ));
174
+ if (!text) {
175
+ lastRawText.current = '';
176
+ setDisplayValue('');
177
+ setNumericValue('');
178
+ onChangeText?.('');
179
+ return;
232
180
  }
233
- if (commit(digits, caretDigit)) {
234
- onChangeText?.(digits);
181
+ const digits = parseInputText(text);
182
+ const raw = lastRawText.current;
183
+ lastRawText.current = text;
184
+ const formattedEdit = findEdit(lastDisplayValue, text);
185
+ const rawEdit = findEdit(raw, text);
186
+ const { old, start, suffix } =
187
+ raw && rawEdit.size < formattedEdit.size ? rawEdit : formattedEdit;
188
+ caretDigitsAfter.current = parseInputText(
189
+ text.slice(text.length - suffix),
190
+ ).length;
191
+ if (text.length < old.length && digits === numericValue) {
192
+ const index = parseInputText(text.slice(0, start)).length - 1;
193
+ if (index >= 0) {
194
+ updateNumericValue(digits.slice(0, index) + digits.slice(index + 1));
195
+ return;
196
+ }
235
197
  }
198
+ updateNumericValue(digits);
236
199
  };
237
200
 
238
- const _onSelectionChange = (e: TextInputSelectionChangeEvent) => {
239
- props.onSelectionChange?.(e);
240
- if (editingRef.current) return;
241
- const selection = e.nativeEvent.selection;
242
- pendingSelectionRef.current = selection;
243
- setTimeout(() => {
244
- if (pendingSelectionRef.current !== selection) return;
245
- pendingSelectionRef.current = null;
246
- const { start, end } = selection;
247
- const display = displayRef.current;
248
- const max = display ? display.length - currency.length : 0;
249
- const clamped = {
250
- start: Math.min(start, max),
251
- end: Math.min(end, max),
252
- };
253
- selectionRef.current = clamped;
254
- if (clamped.start !== start || clamped.end !== end) {
255
- inputRef.current?.setSelection(clamped.start, clamped.end);
256
- }
257
- });
201
+ const focusAtEnd = () => {
202
+ inputRef.current?.focus();
203
+ inputRef.current?.setSelection(displayValue.length, displayValue.length);
258
204
  };
259
205
 
260
206
  const _onFocus = (e: TextInputFocusEvent) => {
261
207
  setFocused(true);
262
- const caret = pendingCaretRef.current;
263
- pendingCaretRef.current = null;
264
- if (caret !== null && !props.selectTextOnFocus) {
265
- selectionRef.current = { start: caret, end: caret };
266
- inputRef.current?.setSelection(caret, caret);
267
- }
268
208
  onFocus?.(e);
269
209
  };
270
210
 
@@ -274,13 +214,15 @@ const InputMoney = forwardRef(
274
214
  };
275
215
 
276
216
  const _setText = (text: string) => {
277
- const digits = text
278
- .replace(/\D/g, '')
279
- .replace(/^0+(?=\d)/, '')
280
- .slice(0, digitLimit);
281
- if (commit(digits, digits.length)) {
282
- onChangeText?.(digits);
217
+ caretDigitsAfter.current = undefined;
218
+ if (!text) {
219
+ setDisplayValue('');
220
+ setNumericValue('');
221
+ onChangeText?.('');
222
+ return;
283
223
  }
224
+ const newNumericValue = parseInputText(text);
225
+ updateNumericValue(newNumericValue);
284
226
  };
285
227
 
286
228
  useImperativeHandle(ref, () => {
@@ -302,6 +244,14 @@ const InputMoney = forwardRef(
302
244
  placeholderColor = disabledColor;
303
245
  iconTintColor = disabledColor;
304
246
  }
247
+ const textStyle = {
248
+ fontSize: scaledFontSize,
249
+ fontFamily: inputFontFamily,
250
+ };
251
+ const placeholderHasCurrency = placeholder.endsWith(currency);
252
+ const inputPlaceholder = placeholderHasCurrency
253
+ ? placeholder.slice(0, -currency.length)
254
+ : placeholder;
305
255
  return (
306
256
  <View
307
257
  style={[
@@ -320,44 +270,46 @@ const InputMoney = forwardRef(
320
270
  onPress={onPressFloatingIcon}
321
271
  componentId={componentId}
322
272
  />
323
- <View style={styles.inputView}>
273
+ <Pressable
274
+ style={[styles.inputView, styles.moneyInputView]}
275
+ onPress={focusAtEnd}
276
+ disabled={disabled}
277
+ >
324
278
  <SystemTextInput
325
279
  {...props}
326
280
  accessibilityLabel={`${componentId}`}
327
281
  textAlignVertical="center"
328
282
  ref={inputRef}
329
- editable={props.editable !== false && !disabled}
330
283
  accessibilityState={{
331
284
  disabled: disabled,
332
285
  ...props.accessibilityState,
333
286
  }}
334
- style={[
335
- {
336
- width: '100%',
337
- paddingLeft: Spacing.M,
338
- height: '100%',
339
- fontSize: scaledFontSize,
340
- paddingVertical: Spacing.S,
341
- fontFamily: inputFontFamily,
342
- },
343
- {
344
- color: textColor,
345
- },
346
- ]}
287
+ style={[textStyle, styles.moneyTextInput, { color: textColor }]}
347
288
  allowFontScaling={false}
348
289
  textBreakStrategy="highQuality"
349
290
  value={displayValue}
291
+ selection={caretSelection ?? props.selection}
350
292
  onChangeText={_onChangeText}
351
- onSelectionChange={_onSelectionChange}
352
293
  onFocus={_onFocus}
353
294
  onBlur={_onBlur}
354
- placeholder={placeholder}
295
+ placeholder={inputPlaceholder}
355
296
  selectionColor={theme.colors.primary}
356
297
  placeholderTextColor={placeholderColor}
357
298
  keyboardType={'numeric'}
358
299
  maxLength={undefined}
359
300
  />
360
- </View>
301
+ {displayValue || placeholderHasCurrency ? (
302
+ <Text
303
+ style={[
304
+ textStyle,
305
+ { color: displayValue ? textColor : placeholderColor },
306
+ ]}
307
+ allowFontScaling={false}
308
+ >
309
+ {currency}
310
+ </Text>
311
+ ) : null}
312
+ </Pressable>
361
313
  <View style={styles.iconView}>
362
314
  {showClearIcon && focused && displayValue ? (
363
315
  <TouchableOpacity
package/Input/styles.ts CHANGED
@@ -218,4 +218,13 @@ export default StyleSheet.create({
218
218
  marginLeft: Spacing.M,
219
219
  },
220
220
  debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
221
+ moneyInputView: {
222
+ paddingLeft: Spacing.M,
223
+ },
224
+ moneyTextInput: {
225
+ flexShrink: 1,
226
+ paddingLeft: 0,
227
+ paddingRight: Spacing.XXS,
228
+ paddingVertical: 0,
229
+ },
221
230
  });
package/Layout/Screen.tsx CHANGED
@@ -64,6 +64,8 @@ import {
64
64
  import { DuoQuickReturnHeader } from '../Application/Components/DuoQuickReturnHeader';
65
65
  import { useDuoTabBarInset } from '../Application/BottomTab/DuoTabBar';
66
66
 
67
+ const ANIMATED_BANNER_HEIGHT = 210;
68
+
67
69
  export interface ScreenProps extends ViewProps {
68
70
  /**
69
71
  * ReactNode representing the content of the screen.
@@ -222,19 +224,6 @@ const Screen = forwardRef(
222
224
  const frame = useSafeAreaFrame();
223
225
  const heightHeader = useHeaderHeight();
224
226
 
225
- /**
226
- * Dual animation source:
227
- * - `animatedValue` is a react-native `Animated.Value` driven natively by the
228
- * scroll event. It is what legacy consumers receive (`animatedValue`).
229
- * - `sharedValue` is a reanimated `SharedValue` consumed by every internal
230
- * foundation component (and offered to modern consumers). It is kept in
231
- * sync from the scroll `listener` (see below), which fires on the JS
232
- * thread even when the scroll uses the native driver.
233
- * If a legacy value is driven outside of Screen, its listener also
234
- * mirrors JS-driven updates into the shared value.
235
- * Consumers pass only `Animated.Value`; the reanimated shared value stays
236
- * an internal implementation detail.
237
- */
238
227
  const animatedValue = useRef<Animated.Value>(
239
228
  customAnimatedValue ?? new Animated.Value(0),
240
229
  );
@@ -258,7 +247,6 @@ const Screen = forwardRef(
258
247
  const currentTint = useRef<string | undefined>(undefined);
259
248
 
260
249
  const isTab = !!screen?.bottomTab;
261
- // Duo floating tab bar overlays the tab content instead of taking layout space
262
250
  const duoTabInset = useDuoTabBarInset();
263
251
 
264
252
  let handleScroll;
@@ -267,14 +255,12 @@ const Screen = forwardRef(
267
255
  const isIOS = Platform.OS === 'ios';
268
256
  const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
269
257
  const hasSideBar = !!layout.sideBar;
270
- // iPhone Duo: content scrolls under a glass header instead of starting below it
271
258
  const useDuoGlass =
272
259
  hasSideBar &&
273
260
  (headerType === 'default' || headerType === 'extended') &&
274
261
  !animatedHeader &&
275
262
  !inputSearchProps;
276
263
 
277
- // iPhone Duo: the header leaves with the content and returns on a reverse scroll
278
264
  const useQuickReturn =
279
265
  hasSideBar && scrollable && (useDuoGlass || !!animatedHeader);
280
266
  const headerHide = useSharedValue(0);
@@ -324,12 +310,14 @@ const Screen = forwardRef(
324
310
  */
325
311
  screen?.onSetParams?.(trackingParams);
326
312
 
313
+ const appliedHeaderMode = useRef<string | undefined>(undefined);
314
+
327
315
  /**
328
316
  * export options for screen
329
317
  * @param headerType
330
318
  */
331
319
  const setHeaderType = useCallback(
332
- (headerType: HeaderType) => {
320
+ (headerType: HeaderType, keepTitle = false) => {
333
321
  let options: StackNavigationOptions;
334
322
  switch (headerType) {
335
323
  case 'none':
@@ -418,6 +406,10 @@ const Screen = forwardRef(
418
406
  }
419
407
  }
420
408
 
409
+ if (keepTitle) {
410
+ delete options.headerTitle;
411
+ }
412
+
421
413
  navigation?.instance?.setOptions({ header: undefined, ...options });
422
414
  },
423
415
  [
@@ -449,10 +441,9 @@ const Screen = forwardRef(
449
441
  headerTransparent: true,
450
442
  headerBackground: (props: any) =>
451
443
  hasSideBar ? (
452
- // glass shows once the banner has scrolled under the header
453
444
  <DuoGlassBackground
454
445
  animatedValue={sharedValue}
455
- fadeStart={Math.max(210 + layoutOffset - heightHeader, 0)}
446
+ bannerBottom={ANIMATED_BANNER_HEIGHT + layoutOffset}
456
447
  />
457
448
  ) : (
458
449
  <HeaderBackground
@@ -486,7 +477,6 @@ const Screen = forwardRef(
486
477
  useShadowHeader,
487
478
  hasSideBar,
488
479
  layoutOffset,
489
- heightHeader,
490
480
  quickReturnOptions,
491
481
  ],
492
482
  );
@@ -521,7 +511,6 @@ const Screen = forwardRef(
521
511
  let searchWidth = layout.width - 24;
522
512
  let searchLeft = Spacing.M;
523
513
  if (layout.sideBar) {
524
- // back floats in the side bar: the search spans the content width
525
514
  searchWidth = layout.contentWidth - Spacing.M * 2;
526
515
  searchLeft = Spacing.M - getHeaderMargin(layout.width);
527
516
  }
@@ -563,10 +552,13 @@ const Screen = forwardRef(
563
552
 
564
553
  if (animatedHeader) {
565
554
  setAnimatedHeader(animatedHeader);
555
+ appliedHeaderMode.current = 'animated';
566
556
  } else if (inputSearchProps) {
567
557
  setAnimatedSearch();
558
+ appliedHeaderMode.current = 'search';
568
559
  } else {
569
- setHeaderType(headerType);
560
+ setHeaderType(headerType, appliedHeaderMode.current === headerType);
561
+ appliedHeaderMode.current = headerType;
570
562
  }
571
563
 
572
564
  return () => {
@@ -613,7 +605,6 @@ const Screen = forwardRef(
613
605
  useNativeDriver: true,
614
606
  listener: (e: NativeSyntheticEvent<NativeScrollEvent>) => {
615
607
  const offsetY = e.nativeEvent.contentOffset.y;
616
- // keep the reanimated source in sync for the internal components.
617
608
  sharedValue.value = offsetY;
618
609
  scrollViewProps?.onScroll?.(e);
619
610
  if (animatedHeader) {
@@ -648,7 +639,6 @@ const Screen = forwardRef(
648
639
  */
649
640
  const snapQuickReturnHeader = (offsetY: number) => {
650
641
  const hide = headerHide.value;
651
- // above heightHeader the header still sits on the content
652
642
  if (
653
643
  !useQuickReturn ||
654
644
  offsetY < heightHeader ||
@@ -692,11 +682,12 @@ const Screen = forwardRef(
692
682
  if (typeof animatedHeader?.component === 'function') {
693
683
  return (
694
684
  <View
695
- style={[styles.screenBanner, { maxHeight: 210 + layoutOffset }]}
685
+ style={[
686
+ styles.screenBanner,
687
+ { maxHeight: ANIMATED_BANNER_HEIGHT + layoutOffset },
688
+ ]}
696
689
  >
697
690
  {animatedHeader?.component({
698
- // legacy consumers read `animatedValue` (Animated.Value),
699
- // modern consumers read `sharedValue` (SharedValue).
700
691
  animatedValue: animatedValue.current,
701
692
  sharedValue: sharedValue,
702
693
  })}
@@ -756,11 +747,6 @@ const Screen = forwardRef(
756
747
  }
757
748
  };
758
749
 
759
- // Cap the screen container height so a scrollable body with a sticky footer
760
- // can't push the footer past the visible frame (web previews measure the
761
- // safe-area frame from the seeded SafeAreaProvider metrics). Subtract the
762
- // bottom tab bar when present and the header unless it's an animated/extended
763
- // header (those overlay rather than offset the body).
764
750
  const getHeight = () => {
765
751
  let newHeight = frame.height;
766
752
  if (isTab) {
@@ -772,8 +758,6 @@ const Screen = forwardRef(
772
758
  return newHeight;
773
759
  };
774
760
 
775
- // without the grid, the content container keeps clear of the horizontal insets.
776
- // Only set when there are insets, so a caller's paddingHorizontal is never overridden.
777
761
  const hasContentInset = layout.insetLeft > 0 || layout.insetRight > 0;
778
762
  const needsContentInset = !useGridLayout && hasContentInset;
779
763
  const contentInsetStyle = needsContentInset && {
@@ -796,8 +780,6 @@ const Screen = forwardRef(
796
780
  Styles.flex,
797
781
  {
798
782
  backgroundColor: backgroundColor ?? theme.colors.background.default,
799
- // Web-only cap so a scrollable body with a sticky footer can't push
800
- // the footer past the frame. Native handles this without a cap.
801
783
  maxHeight: Platform.OS === 'web' ? getHeight() : undefined,
802
784
  },
803
785
  ]}
@@ -882,7 +864,6 @@ const Screen = forwardRef(
882
864
  )}
883
865
 
884
866
  {Footer && (
885
- // with side insets, the clip keeps the shadow off the side bar, only the top one shows
886
867
  <View
887
868
  pointerEvents={'box-none'}
888
869
  style={[
@@ -904,8 +885,6 @@ const Screen = forwardRef(
904
885
  },
905
886
  ]}
906
887
  >
907
- {/* The footer inset is a caller decision, not a platform one — a footer
908
- that carries its own horizontal padding passes `marginFooter={false}`. */}
909
888
  <Section useMargin={marginFooter}>{Footer}</Section>
910
889
  </View>
911
890
  </View>
package/Layout/utils.ts CHANGED
@@ -510,6 +510,7 @@ const useGridSystem = () => {
510
510
  export {
511
511
  DuoMetrics,
512
512
  HeaderMetrics,
513
+ InnerEdgeStyles,
513
514
  getHeaderMargin,
514
515
  getHeaderRowOptions,
515
516
  useGridSystem,