react-native-terra-ui 0.2.2 → 0.4.0

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 (75) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/lib/module/components/chip/Chip.js +97 -41
  3. package/lib/module/components/chip/Chip.js.map +1 -1
  4. package/lib/module/components/chip/utils.js +3 -1
  5. package/lib/module/components/chip/utils.js.map +1 -1
  6. package/lib/module/components/header/variants/LargeTitleHeader/index.js +50 -16
  7. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  8. package/lib/module/components/screen/ScreenFlashList.js +32 -26
  9. package/lib/module/components/screen/ScreenFlashList.js.map +1 -1
  10. package/lib/module/components/screen/ScreenFlatList.js +37 -31
  11. package/lib/module/components/screen/ScreenFlatList.js.map +1 -1
  12. package/lib/module/components/screen/ScreenScrollView.js +24 -18
  13. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  14. package/lib/module/components/text/Text.js +2 -0
  15. package/lib/module/components/text/Text.js.map +1 -1
  16. package/lib/module/context/ThemeProvider.js +15 -0
  17. package/lib/module/context/ThemeProvider.js.map +1 -1
  18. package/lib/module/context/index.js +1 -1
  19. package/lib/module/context/index.js.map +1 -1
  20. package/lib/module/theme/breakpoints.js +29 -0
  21. package/lib/module/theme/breakpoints.js.map +1 -0
  22. package/lib/module/theme/index.js +4 -0
  23. package/lib/module/theme/index.js.map +1 -1
  24. package/lib/module/theme/registry.js +3 -1
  25. package/lib/module/theme/registry.js.map +1 -1
  26. package/lib/module/theme/screen-margin.js +17 -0
  27. package/lib/module/theme/screen-margin.js.map +1 -0
  28. package/lib/module/theme/tokens/dark.js +3 -3
  29. package/lib/module/theme/tokens/dark.js.map +1 -1
  30. package/lib/module/theme/tokens/primitives.js +12 -2
  31. package/lib/module/theme/tokens/primitives.js.map +1 -1
  32. package/lib/typescript/src/components/chip/Chip.d.ts.map +1 -1
  33. package/lib/typescript/src/components/chip/utils.d.ts.map +1 -1
  34. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +9 -3
  35. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  36. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts.map +1 -1
  37. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts.map +1 -1
  38. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  39. package/lib/typescript/src/components/text/Text.d.ts +1 -0
  40. package/lib/typescript/src/components/text/Text.d.ts.map +1 -1
  41. package/lib/typescript/src/context/ThemeProvider.d.ts +8 -0
  42. package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
  43. package/lib/typescript/src/context/index.d.ts +1 -1
  44. package/lib/typescript/src/context/index.d.ts.map +1 -1
  45. package/lib/typescript/src/theme/breakpoints.d.ts +32 -0
  46. package/lib/typescript/src/theme/breakpoints.d.ts.map +1 -0
  47. package/lib/typescript/src/theme/index.d.ts +4 -0
  48. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  49. package/lib/typescript/src/theme/registry.d.ts.map +1 -1
  50. package/lib/typescript/src/theme/screen-margin.d.ts +15 -0
  51. package/lib/typescript/src/theme/screen-margin.d.ts.map +1 -0
  52. package/lib/typescript/src/theme/tokens/primitives.d.ts +10 -2
  53. package/lib/typescript/src/theme/tokens/primitives.d.ts.map +1 -1
  54. package/lib/typescript/src/theme/types.d.ts +9 -3
  55. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  56. package/package.json +1 -2
  57. package/src/components/chip/Chip.tsx +94 -48
  58. package/src/components/chip/utils.ts +8 -1
  59. package/src/components/header/variants/LargeTitleHeader/index.tsx +56 -15
  60. package/src/components/screen/ScreenFlashList.tsx +37 -30
  61. package/src/components/screen/ScreenFlatList.tsx +41 -34
  62. package/src/components/screen/ScreenScrollView.tsx +24 -18
  63. package/src/components/text/Text.tsx +3 -0
  64. package/src/context/ThemeProvider.tsx +15 -0
  65. package/src/context/index.ts +1 -1
  66. package/src/theme/breakpoints.ts +41 -0
  67. package/src/theme/index.ts +5 -0
  68. package/src/theme/registry.ts +2 -1
  69. package/src/theme/screen-margin.ts +26 -0
  70. package/src/theme/tokens/dark.ts +3 -3
  71. package/src/theme/tokens/primitives.ts +12 -2
  72. package/src/theme/types.ts +12 -2
  73. package/skills/terra-ui/SKILL.md +0 -193
  74. package/skills/terra-ui/references/recipes.md +0 -198
  75. package/skills/terra-ui/references/troubleshooting.md +0 -112
@@ -6,14 +6,19 @@ import {
6
6
  type ReactNode,
7
7
  useContext,
8
8
  } from 'react';
9
- import { View, type ViewProps } from 'react-native';
9
+ import { Text as RNText, View, type ViewProps } from 'react-native';
10
10
 
11
- import { StyleSheet, withUnistyles } from 'react-native-unistyles';
11
+ import {
12
+ StyleSheet,
13
+ useUnistyles,
14
+ withUnistyles,
15
+ } from 'react-native-unistyles';
12
16
 
13
- import type { ColorToken, TerraIconName, TextVariant } from '#theme/types';
17
+ import type { ColorToken, RoleTextVariant, TerraIconName } from '#theme/types';
18
+ import { resolveThemeColor } from '#utils/resolve-theme-color';
19
+ import { FONT_WEIGHT_VALUE, SYSTEM_FONT } from '#utils/typography';
14
20
 
15
21
  import { Icon } from '../icon';
16
- import { Text } from '../text';
17
22
  import type { ChipColor, ChipSize, ChipVariant } from './types';
18
23
  import { type CustomChipColors, getCustomColorColors } from './utils';
19
24
 
@@ -69,6 +74,13 @@ const SCHEME_NAMES = new Set([
69
74
  type ChipScheme = 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
70
75
  type ChipPaintedVariant = Exclude<ChipVariant, 'ghost'>;
71
76
 
77
+ // `ghost` is `outline` with its border taken away, so it has no colors of its
78
+ // own — it borrows outline's foreground. Only the *paint* differs, and that
79
+ // comes from the `ghost` style variant, never from here.
80
+ function paintedVariant(variant: ChipVariant): ChipPaintedVariant {
81
+ return variant === 'ghost' ? 'outline' : variant;
82
+ }
83
+
72
84
  /** Foreground token per (scheme, variant) — mirrors the spec's token table. */
73
85
  const FG_TOKEN: Record<ChipScheme, Record<ChipPaintedVariant, ColorToken>> = {
74
86
  primary: {
@@ -105,33 +117,47 @@ function resolveForeground(
105
117
  customColors: CustomChipColors | null
106
118
  ): ColorToken {
107
119
  if (customColors) return customColors.fg;
108
- if (variant === 'ghost') return 'text.default';
109
- return FG_TOKEN[color as ChipScheme][variant];
120
+ return FG_TOKEN[color as ChipScheme][paintedVariant(variant)];
110
121
  }
111
122
 
112
123
  // ─── Label sub-component ───────────────────────────────────────────────────────
113
124
 
114
- const LABEL_VARIANT: Record<ChipSize, TextVariant> = {
125
+ const LABEL_VARIANT: Record<ChipSize, RoleTextVariant> = {
115
126
  sm: 'label-sm',
116
127
  md: 'label-md',
117
128
  lg: 'label-lg',
118
129
  };
119
130
 
131
+ // Renders through React Native's `Text` rather than the `Text` primitive: a
132
+ // chip label never needs the primitive's alignment, decoration, or truncation
133
+ // handling, and a list of chips pays for that style assembly on every render.
134
+ // The type scale lives in `styles.label`'s size variants, so unistyles resolves
135
+ // it against the live theme, and the colour is resolved here from the token the
136
+ // root put on the context — keeping the theme-change behaviour the primitive
137
+ // gave us.
120
138
  const ChipLabel = ({ children }: { children: ReactNode }) => {
121
139
  const { color, size } = useContext(ChipContext);
140
+ const { theme } = useUnistyles();
141
+ styles.useVariants({ size });
142
+
122
143
  return (
123
- <Text variant={LABEL_VARIANT[size]} weight="semibold" color={color}>
144
+ <RNText
145
+ style={[styles.label, { color: resolveThemeColor(color, theme) }]}
146
+ maxFontSizeMultiplier={
147
+ theme.typography.variants[LABEL_VARIANT[size]].maxFontSizeMultiplier
148
+ }
149
+ >
124
150
  {children}
125
- </Text>
151
+ </RNText>
126
152
  );
127
153
  };
128
154
 
129
155
  // ─── Icon sub-component ────────────────────────────────────────────────────────
130
156
 
131
157
  const ICON_SIZE: Record<ChipSize, number> = {
132
- sm: 12,
133
- md: 14,
134
- lg: 16,
158
+ sm: 16,
159
+ md: 18,
160
+ lg: 20,
135
161
  };
136
162
 
137
163
  export interface ChipIconProps extends Omit<ViewProps, 'children'> {
@@ -157,7 +183,7 @@ const ChipIcon = ({
157
183
  color={color || contextColor}
158
184
  size={sizeProp ?? ICON_SIZE[size]}
159
185
  strokeWidth={strokeWidth}
160
- style={style}
186
+ style={[{ marginVertical: -4 }, style]}
161
187
  {...rest}
162
188
  />
163
189
  );
@@ -193,30 +219,27 @@ const ChipRoot = forwardRef<ComponentRef<typeof View>, ChipProps>(function Chip(
193
219
  // literal-union type while still comparing against the real value below.
194
220
  styles.useVariants({ color: color as ChipScheme, variant, size });
195
221
 
196
- const isCustomColor = variant !== 'ghost' && !SCHEME_NAMES.has(color);
222
+ const isCustomColor = !SCHEME_NAMES.has(color);
197
223
  const customColors = isCustomColor
198
- ? getCustomColorColors(color, variant as 'solid' | 'soft' | 'outline')
224
+ ? getCustomColorColors(color, paintedVariant(variant))
199
225
  : null;
200
226
 
201
227
  const fg = resolveForeground(color, variant, customColors);
228
+ // Ghost takes outline's foreground but none of its paint — its transparent
229
+ // background and absent border come from the `ghost` style variant, so
230
+ // applying the derived paint here would hand it outline's border back.
231
+ const customPaint =
232
+ customColors && variant !== 'ghost'
233
+ ? {
234
+ backgroundColor: customColors.bg,
235
+ borderColor: customColors.border,
236
+ borderWidth: customColors.borderWidth,
237
+ }
238
+ : null;
202
239
 
203
240
  return (
204
241
  <ChipContext.Provider value={{ color: fg, size }}>
205
- <View
206
- ref={ref}
207
- style={[
208
- styles.base,
209
- customColors
210
- ? {
211
- backgroundColor: customColors.bg,
212
- borderColor: customColors.border,
213
- borderWidth: customColors.borderWidth,
214
- }
215
- : null,
216
- style,
217
- ]}
218
- {...rest}
219
- >
242
+ <View ref={ref} style={[styles.base, customPaint, style]} {...rest}>
220
243
  {renderChildren(children)}
221
244
  </View>
222
245
  </ChipContext.Provider>
@@ -256,8 +279,41 @@ Chip.Icon = ChipIcon;
256
279
  // this View, and is handed to them as a token via `FG_TOKEN` / context.
257
280
  const styles = StyleSheet.create((theme) => {
258
281
  const c = theme.color;
282
+ // A custom font encodes its weight in the family name, so only the system
283
+ // font takes a numeric `fontWeight` — the same branch `Text` makes. Inlined
284
+ // rather than extracted: the unistyles Babel plugin rewrites this factory,
285
+ // and an imported function called from inside it is not in scope at runtime.
286
+ const labelFont = theme.typography.fonts.semibold;
287
+ const isSystemLabelFont = labelFont === SYSTEM_FONT;
259
288
 
260
289
  return {
290
+ // Spelled out rather than derived from `LABEL_VARIANT`: the unistyles Babel
291
+ // plugin reads this object statically, so computed variant keys are not
292
+ // safe here even though they would resolve correctly under test.
293
+ label: {
294
+ paddingHorizontal: 2,
295
+ fontFamily: isSystemLabelFont ? undefined : labelFont,
296
+ fontWeight: isSystemLabelFont ? FONT_WEIGHT_VALUE.semibold : undefined,
297
+ variants: {
298
+ size: {
299
+ sm: {
300
+ fontSize: theme.typography.variants['label-sm'].fontSize,
301
+ lineHeight: theme.typography.variants['label-sm'].lineHeight,
302
+ letterSpacing: theme.typography.variants['label-sm'].letterSpacing,
303
+ },
304
+ md: {
305
+ fontSize: theme.typography.variants['label-md'].fontSize,
306
+ lineHeight: theme.typography.variants['label-md'].lineHeight,
307
+ letterSpacing: theme.typography.variants['label-md'].letterSpacing,
308
+ },
309
+ lg: {
310
+ fontSize: theme.typography.variants['label-lg'].fontSize,
311
+ lineHeight: theme.typography.variants['label-lg'].lineHeight,
312
+ letterSpacing: theme.typography.variants['label-lg'].letterSpacing,
313
+ },
314
+ },
315
+ },
316
+ },
261
317
  base: {
262
318
  flexDirection: 'row',
263
319
  alignItems: 'center',
@@ -268,19 +324,19 @@ const styles = StyleSheet.create((theme) => {
268
324
  sm: {
269
325
  minHeight: 24,
270
326
  paddingVertical: 6,
271
- paddingHorizontal: 12,
327
+ paddingHorizontal: 8,
272
328
  gap: 4,
273
329
  },
274
330
  md: {
275
331
  minHeight: 26,
276
332
  paddingVertical: 8,
277
- paddingHorizontal: 14,
333
+ paddingHorizontal: 10,
278
334
  gap: 4,
279
335
  },
280
336
  lg: {
281
337
  minHeight: 28,
282
338
  paddingVertical: 8,
283
- paddingHorizontal: 16,
339
+ paddingHorizontal: 12,
284
340
  gap: 4,
285
341
  },
286
342
  },
@@ -308,7 +364,6 @@ const styles = StyleSheet.create((theme) => {
308
364
  variant: 'solid',
309
365
  styles: {
310
366
  backgroundColor: c['action.bg.primary'],
311
- borderColor: c['action.bg.primary'],
312
367
  borderWidth: 0,
313
368
  },
314
369
  },
@@ -317,7 +372,6 @@ const styles = StyleSheet.create((theme) => {
317
372
  variant: 'soft',
318
373
  styles: {
319
374
  backgroundColor: c['action.bg.primary.subtle'],
320
- borderColor: c['action.bg.primary.subtle'],
321
375
  borderWidth: 0,
322
376
  },
323
377
  },
@@ -326,7 +380,7 @@ const styles = StyleSheet.create((theme) => {
326
380
  variant: 'outline',
327
381
  styles: {
328
382
  backgroundColor: 'transparent',
329
- borderColor: c['border.accent'],
383
+ borderColor: c['action.bg.primary.subtle'],
330
384
  borderWidth: 1,
331
385
  },
332
386
  },
@@ -335,7 +389,6 @@ const styles = StyleSheet.create((theme) => {
335
389
  variant: 'solid',
336
390
  styles: {
337
391
  backgroundColor: c['action.bg.secondary'],
338
- borderColor: c['action.bg.secondary'],
339
392
  borderWidth: 0,
340
393
  },
341
394
  },
@@ -344,7 +397,6 @@ const styles = StyleSheet.create((theme) => {
344
397
  variant: 'soft',
345
398
  styles: {
346
399
  backgroundColor: c['action.bg.secondary.subtle'],
347
- borderColor: c['action.bg.secondary.subtle'],
348
400
  borderWidth: 0,
349
401
  },
350
402
  },
@@ -353,7 +405,7 @@ const styles = StyleSheet.create((theme) => {
353
405
  variant: 'outline',
354
406
  styles: {
355
407
  backgroundColor: 'transparent',
356
- borderColor: c['border.default'],
408
+ borderColor: c['action.bg.secondary.subtle'],
357
409
  borderWidth: 1,
358
410
  },
359
411
  },
@@ -362,7 +414,6 @@ const styles = StyleSheet.create((theme) => {
362
414
  variant: 'solid',
363
415
  styles: {
364
416
  backgroundColor: c['status.success.bg'],
365
- borderColor: c['status.success.bg'],
366
417
  borderWidth: 0,
367
418
  },
368
419
  },
@@ -371,7 +422,6 @@ const styles = StyleSheet.create((theme) => {
371
422
  variant: 'soft',
372
423
  styles: {
373
424
  backgroundColor: c['status.success.bg.subtle'],
374
- borderColor: c['status.success.bg.subtle'],
375
425
  borderWidth: 0,
376
426
  },
377
427
  },
@@ -380,7 +430,7 @@ const styles = StyleSheet.create((theme) => {
380
430
  variant: 'outline',
381
431
  styles: {
382
432
  backgroundColor: 'transparent',
383
- borderColor: c['status.success.border'],
433
+ borderColor: c['status.success.bg.subtle'],
384
434
  borderWidth: 1,
385
435
  },
386
436
  },
@@ -389,7 +439,6 @@ const styles = StyleSheet.create((theme) => {
389
439
  variant: 'solid',
390
440
  styles: {
391
441
  backgroundColor: c['status.warning.bg'],
392
- borderColor: c['status.warning.bg'],
393
442
  borderWidth: 0,
394
443
  },
395
444
  },
@@ -398,7 +447,6 @@ const styles = StyleSheet.create((theme) => {
398
447
  variant: 'soft',
399
448
  styles: {
400
449
  backgroundColor: c['status.warning.bg.subtle'],
401
- borderColor: c['status.warning.bg.subtle'],
402
450
  borderWidth: 0,
403
451
  },
404
452
  },
@@ -407,7 +455,7 @@ const styles = StyleSheet.create((theme) => {
407
455
  variant: 'outline',
408
456
  styles: {
409
457
  backgroundColor: 'transparent',
410
- borderColor: c['status.warning.border'],
458
+ borderColor: c['status.warning.bg.subtle'],
411
459
  borderWidth: 1,
412
460
  },
413
461
  },
@@ -416,7 +464,6 @@ const styles = StyleSheet.create((theme) => {
416
464
  variant: 'solid',
417
465
  styles: {
418
466
  backgroundColor: c['status.danger.bg'],
419
- borderColor: c['status.danger.bg'],
420
467
  borderWidth: 0,
421
468
  },
422
469
  },
@@ -425,7 +472,6 @@ const styles = StyleSheet.create((theme) => {
425
472
  variant: 'soft',
426
473
  styles: {
427
474
  backgroundColor: c['status.danger.bg.subtle'],
428
- borderColor: c['status.danger.bg.subtle'],
429
475
  borderWidth: 0,
430
476
  },
431
477
  },
@@ -434,7 +480,7 @@ const styles = StyleSheet.create((theme) => {
434
480
  variant: 'outline',
435
481
  styles: {
436
482
  backgroundColor: 'transparent',
437
- borderColor: c['status.danger.border'],
483
+ borderColor: c['status.danger.bg.subtle'],
438
484
  borderWidth: 1,
439
485
  },
440
486
  },
@@ -18,8 +18,15 @@ export function getCustomColorColors(
18
18
  literal: string,
19
19
  variant: 'solid' | 'soft' | 'outline'
20
20
  ): CustomChipColors {
21
+ // Outline borrows the `soft` background as its border, so the two variants
22
+ // read as the same weight of colour — one filled, one drawn.
21
23
  if (variant === 'outline') {
22
- return { bg: 'transparent', border: literal, borderWidth: 1, fg: literal };
24
+ return {
25
+ bg: 'transparent',
26
+ border: withAlpha(literal, CUSTOM_COLOR_SOFT_ALPHA),
27
+ borderWidth: 1,
28
+ fg: literal,
29
+ };
23
30
  }
24
31
  if (variant === 'soft') {
25
32
  const bg = withAlpha(literal, CUSTOM_COLOR_SOFT_ALPHA);
@@ -29,17 +29,23 @@ const COMPACT_TITLE_SLIDE_OFFSET = 8;
29
29
  const LARGE_TITLE_SLIDE_OFFSET = 6;
30
30
 
31
31
  export interface LargeTitleHeaderProps extends TitleHeaderProps {
32
+ /** Optional small line shown above the large title. */
33
+ overline?: string;
32
34
  /** Optional secondary line shown under the large title. */
33
- caption?: string;
35
+ subtitle?: string;
34
36
  /** Hide the large title (keeps the compact bar). Defaults to `false`. */
35
37
  isLargeTitleHidden?: boolean;
36
38
  }
37
39
 
38
40
  function LargeTitlePortalContent({
39
41
  title,
40
- caption,
42
+ overline,
43
+ subtitle,
41
44
  titleAlignment = 'center',
42
- }: Pick<LargeTitleHeaderProps, 'title' | 'caption' | 'titleAlignment'>) {
45
+ }: Pick<
46
+ LargeTitleHeaderProps,
47
+ 'title' | 'overline' | 'subtitle' | 'titleAlignment'
48
+ >) {
43
49
  const { theme } = useUnistyles();
44
50
  const { scrollY } = useScreen();
45
51
  styles.useVariants({ titleAlignment });
@@ -69,6 +75,16 @@ function LargeTitlePortalContent({
69
75
 
70
76
  return (
71
77
  <Animated.View style={[styles.portalContent, largeTitleAnimatedStyle]}>
78
+ {!!overline && (
79
+ <Text
80
+ style={styles.overline}
81
+ maxFontSizeMultiplier={
82
+ theme.typography.variants['label-sm'].maxFontSizeMultiplier
83
+ }
84
+ >
85
+ {overline}
86
+ </Text>
87
+ )}
72
88
  <Text
73
89
  style={styles.largeTitle}
74
90
  maxFontSizeMultiplier={
@@ -77,14 +93,14 @@ function LargeTitlePortalContent({
77
93
  >
78
94
  {title}
79
95
  </Text>
80
- {!!caption && (
96
+ {!!subtitle && (
81
97
  <Text
82
- style={styles.caption}
98
+ style={styles.subtitle}
83
99
  maxFontSizeMultiplier={
84
100
  theme.typography.variants['body-lg'].maxFontSizeMultiplier
85
101
  }
86
102
  >
87
- {caption}
103
+ {subtitle}
88
104
  </Text>
89
105
  )}
90
106
  </Animated.View>
@@ -103,13 +119,18 @@ function LargeTitlePortalContent({
103
119
  * @example
104
120
  * ```tsx
105
121
  * <Screen.Header>
106
- * <Header.LargeTitle title="Meditate" caption="Good morning" />
122
+ * <Header.LargeTitle
123
+ * overline="Today"
124
+ * title="Meditate"
125
+ * subtitle="Good morning"
126
+ * />
107
127
  * </Screen.Header>
108
128
  * ```
109
129
  */
110
130
  export function LargeTitleHeader({
111
131
  title,
112
- caption,
132
+ overline,
133
+ subtitle,
113
134
  LeftToolbar,
114
135
  RightToolbar,
115
136
  dismissAction = 'none',
@@ -181,11 +202,6 @@ export function LargeTitleHeader({
181
202
  isLargeTitleHiddenValue.value = isLargeTitleHidden;
182
203
  }, [isLargeTitleHidden, isLargeTitleHiddenValue]);
183
204
 
184
- // // biome-ignore lint/correctness/useExhaustiveDependencies: reset stale collapse height whenever content that resizes the portal (title/caption/visibility) changes, ahead of remeasurement
185
- // useEffect(() => {
186
- // setHeaderCollapseHeight(0);
187
- // }, [title, caption, isLargeTitleHidden, setHeaderCollapseHeight]);
188
-
189
205
  const leading =
190
206
  dismissAction === 'back' ? (
191
207
  <>
@@ -220,7 +236,8 @@ export function LargeTitleHeader({
220
236
  <Portal hostName={SCREEN_HEADER_PORTAL_HOST}>
221
237
  <LargeTitlePortalContent
222
238
  title={title}
223
- caption={caption}
239
+ overline={overline}
240
+ subtitle={subtitle}
224
241
  titleAlignment={titleAlignment}
225
242
  />
226
243
  </Portal>
@@ -288,11 +305,29 @@ const styles = StyleSheet.create((theme, runtime) => ({
288
305
  justifyContent: 'flex-end',
289
306
  },
290
307
  portalContent: {
308
+ gap: 2,
291
309
  minHeight: theme.layout.header.height,
292
310
  // paddingTop: theme.spacing['1'],
293
311
  paddingBottom: theme.spacing['3'],
294
312
  paddingHorizontal: theme.layout.screen.margin.x,
295
313
  },
314
+ overline: {
315
+ fontSize: theme.typography.variants['label-sm'].fontSize,
316
+ lineHeight: theme.typography.variants['label-sm'].lineHeight,
317
+ letterSpacing: theme.typography.variants['label-sm'].letterSpacing,
318
+ fontWeight:
319
+ FONT_WEIGHT_VALUE[theme.typography.variants['label-sm'].fontWeight],
320
+ color:
321
+ (theme.color as unknown as Record<string, string | undefined>)[
322
+ 'text.subtle'
323
+ ] ?? '',
324
+ variants: {
325
+ titleAlignment: {
326
+ center: { textAlign: 'center' },
327
+ left: { textAlign: 'left' },
328
+ },
329
+ },
330
+ },
296
331
  largeTitle: {
297
332
  fontSize: theme.typography.variants['headline-lg'].fontSize,
298
333
  lineHeight: theme.typography.variants['headline-lg'].lineHeight,
@@ -303,8 +338,14 @@ const styles = StyleSheet.create((theme, runtime) => ({
303
338
  (theme.color as unknown as Record<string, string | undefined>)[
304
339
  'text.default'
305
340
  ] ?? '',
341
+ variants: {
342
+ titleAlignment: {
343
+ center: { textAlign: 'center' },
344
+ left: { textAlign: 'left' },
345
+ },
346
+ },
306
347
  },
307
- caption: {
348
+ subtitle: {
308
349
  fontSize: theme.typography.variants['body-lg'].fontSize,
309
350
  lineHeight: theme.typography.variants['body-lg'].lineHeight,
310
351
  letterSpacing: theme.typography.variants['body-lg'].letterSpacing,
@@ -12,6 +12,7 @@ import Animated, {
12
12
 
13
13
  import { StyleSheet } from 'react-native-unistyles';
14
14
 
15
+ import { resolveScreenMargin } from '#theme/screen-margin';
15
16
  import { PortalHost } from '../portal';
16
17
  import { BottomSafeArea } from './parts/BottomSafeArea';
17
18
  import { renderScreenPlaceholder } from './parts/ScreenPlaceholder';
@@ -164,33 +165,39 @@ export function ScreenFlashList<T>({
164
165
  );
165
166
  }
166
167
 
167
- const styles = StyleSheet.create((theme, runtime) => ({
168
- content: (
169
- margins: ScreenMargins,
170
- hasHeader: boolean,
171
- bottomInset: number | undefined
172
- ) => {
173
- return {
174
- flexGrow: 1,
175
- // The header is absolutely positioned, so the list reserves its full
176
- // height — bar plus top safe area. This assumes the scroll view does not
177
- // also get UIKit's automatic inset adjustment: on a native tab screen
178
- // that means `disableAutomaticContentInsets` on the tab, or the two
179
- // insets are counted twice.
180
- paddingTop:
181
- (hasHeader ? theme.layout.header.height + runtime.insets.top : 0) +
182
- (hasYMargin(margins) ? theme.layout.screen.margin.y : 0),
183
- paddingHorizontal: hasXMargin(margins) ? theme.layout.screen.margin.x : 0,
184
- // The safe area is `BottomSafeArea`'s job; counting it here as well
185
- // would leave a gap the height of the gesture bar.
186
- paddingBottom:
187
- bottomInset ?? (hasYMargin(margins) ? theme.layout.screen.margin.y : 0),
188
- };
189
- },
190
- safeArea: {
191
- height: runtime.insets.top,
192
- },
193
- headerWrapper: (margins: ScreenMargins) => ({
194
- marginHorizontal: hasXMargin(margins) ? -theme.layout.screen.margin.x : 0,
195
- }),
196
- }));
168
+ const styles = StyleSheet.create((theme, runtime) => {
169
+ const { x: marginX, y: marginY } = resolveScreenMargin(
170
+ theme,
171
+ runtime.breakpoint
172
+ );
173
+
174
+ return {
175
+ content: (
176
+ margins: ScreenMargins,
177
+ hasHeader: boolean,
178
+ bottomInset: number | undefined
179
+ ) => {
180
+ return {
181
+ flexGrow: 1,
182
+ // The header is absolutely positioned, so the list reserves its full
183
+ // height — bar plus top safe area. This assumes the scroll view does not
184
+ // also get UIKit's automatic inset adjustment: on a native tab screen
185
+ // that means `disableAutomaticContentInsets` on the tab, or the two
186
+ // insets are counted twice.
187
+ paddingTop:
188
+ (hasHeader ? theme.layout.header.height + runtime.insets.top : 0) +
189
+ (hasYMargin(margins) ? marginY : 0),
190
+ paddingHorizontal: hasXMargin(margins) ? marginX : 0,
191
+ // The safe area is `BottomSafeArea`'s job; counting it here as well
192
+ // would leave a gap the height of the gesture bar.
193
+ paddingBottom: bottomInset ?? (hasYMargin(margins) ? marginY : 0),
194
+ };
195
+ },
196
+ safeArea: {
197
+ height: runtime.insets.top,
198
+ },
199
+ headerWrapper: (margins: ScreenMargins) => ({
200
+ marginHorizontal: hasXMargin(margins) ? -marginX : 0,
201
+ }),
202
+ };
203
+ });
@@ -12,6 +12,7 @@ import Animated, {
12
12
  } from 'react-native-reanimated';
13
13
  import { StyleSheet } from 'react-native-unistyles';
14
14
 
15
+ import { resolveScreenMargin } from '#theme/screen-margin';
15
16
  import { PortalHost } from '../portal';
16
17
  import { BottomSafeArea } from './parts/BottomSafeArea';
17
18
  import { renderScreenPlaceholder } from './parts/ScreenPlaceholder';
@@ -146,37 +147,43 @@ export function ScreenFlatList<T>({
146
147
  );
147
148
  }
148
149
 
149
- const styles = StyleSheet.create((theme, runtime) => ({
150
- scrollContent: {
151
- flexGrow: 1,
152
- },
153
- content: (
154
- margins: ScreenMargins,
155
- hasHeader: boolean,
156
- bottomInset: number | undefined
157
- ) => ({
158
- flexGrow: 1,
159
- // The header is absolutely positioned, so the list reserves its full
160
- // height — bar plus top safe area. This assumes the scroll view does not
161
- // also get UIKit's automatic inset adjustment: on a native tab screen
162
- // that means `disableAutomaticContentInsets` on the tab, or the two
163
- // insets are counted twice.
164
- paddingTop:
165
- (hasHeader ? theme.layout.header.height + runtime.insets.top : 0) +
166
- (hasYMargin(margins) ? theme.layout.screen.margin.y : 0),
167
- paddingHorizontal: hasXMargin(margins) ? theme.layout.screen.margin.x : 0,
168
- // The safe area is `BottomSafeArea`'s job; counting it here as well would
169
- // leave a gap the height of the gesture bar.
170
- paddingBottom:
171
- bottomInset ?? (hasYMargin(margins) ? theme.layout.screen.margin.y : 0),
172
- }),
173
- safeArea: {
174
- height: runtime.insets.top,
175
- },
176
- headerWrapper: (margins: ScreenMargins) => ({
177
- marginHorizontal: hasXMargin(margins) ? -theme.layout.screen.margin.x : 0,
178
- }),
179
- header: {
180
- height: theme.layout.header.height,
181
- },
182
- }));
150
+ const styles = StyleSheet.create((theme, runtime) => {
151
+ const { x: marginX, y: marginY } = resolveScreenMargin(
152
+ theme,
153
+ runtime.breakpoint
154
+ );
155
+
156
+ return {
157
+ scrollContent: {
158
+ flexGrow: 1,
159
+ },
160
+ content: (
161
+ margins: ScreenMargins,
162
+ hasHeader: boolean,
163
+ bottomInset: number | undefined
164
+ ) => ({
165
+ flexGrow: 1,
166
+ // The header is absolutely positioned, so the list reserves its full
167
+ // height — bar plus top safe area. This assumes the scroll view does not
168
+ // also get UIKit's automatic inset adjustment: on a native tab screen
169
+ // that means `disableAutomaticContentInsets` on the tab, or the two
170
+ // insets are counted twice.
171
+ paddingTop:
172
+ (hasHeader ? theme.layout.header.height + runtime.insets.top : 0) +
173
+ (hasYMargin(margins) ? marginY : 0),
174
+ paddingHorizontal: hasXMargin(margins) ? marginX : 0,
175
+ // The safe area is `BottomSafeArea`'s job; counting it here as well
176
+ // would leave a gap the height of the gesture bar.
177
+ paddingBottom: bottomInset ?? (hasYMargin(margins) ? marginY : 0),
178
+ }),
179
+ safeArea: {
180
+ height: runtime.insets.top,
181
+ },
182
+ headerWrapper: (margins: ScreenMargins) => ({
183
+ marginHorizontal: hasXMargin(margins) ? -marginX : 0,
184
+ }),
185
+ header: {
186
+ height: theme.layout.header.height,
187
+ },
188
+ };
189
+ });