@proulr/sdui-components 0.1.10 → 0.1.12

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": "@proulr/sdui-components",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "main": "./src/renderer/index.ts",
5
5
  "types": "./src/renderer/index.ts",
6
6
  "exports": {
@@ -9,7 +9,7 @@
9
9
  "./renderer": "./src/renderer/index.ts",
10
10
  "./ui": "./src/renderer/ui/index.ts",
11
11
  "./primitives": "./src/renderer/ui/primitives.ts",
12
- "./theme": "./src/renderer/theme/proulr-theme.ts"
12
+ "./theme": "./src/renderer/theme/index.ts"
13
13
  },
14
14
  "files": [
15
15
  "src"
@@ -32,6 +32,7 @@
32
32
  "@proulr/sdui-contract": "^0.1.0"
33
33
  },
34
34
  "peerDependencies": {
35
+ "expo-blur": ">=15",
35
36
  "expo-linear-gradient": ">=56",
36
37
  "react": ">=19",
37
38
  "react-native": ">=0.85",
@@ -48,6 +49,9 @@
48
49
  "expo-linear-gradient": {
49
50
  "optional": true
50
51
  },
52
+ "expo-blur": {
53
+ "optional": true
54
+ },
51
55
  "react-native-google-mobile-ads": {
52
56
  "optional": true
53
57
  },
@@ -60,6 +64,7 @@
60
64
  "@types/react": "~19.2.2",
61
65
  "eslint": "^9.0.0",
62
66
  "eslint-config-expo": "~56.0.4",
67
+ "expo-blur": "~56.0.3",
63
68
  "expo-linear-gradient": "~56.0.4",
64
69
  "globals": "^16.0.0",
65
70
  "husky": "^9.1.7",
@@ -1,11 +1,21 @@
1
+ import type { ReactElement } from 'react';
2
+
1
3
  export type NativeAdPlacementConfig = {
2
4
  aspectRatio?: number;
3
5
  };
4
6
 
7
+ export type NativeAdWebSlotProps = {
8
+ placement: string;
9
+ width?: number;
10
+ aspectRatio: number;
11
+ };
12
+
5
13
  export type NativeAdCatalog = {
6
14
  isEnabled: () => boolean;
7
15
  resolveAdUnitId: (placement: string, testNativeUnitId: string) => string;
8
16
  getPlacementConfig?: (placement: string) => NativeAdPlacementConfig | undefined;
17
+ /** Web-only: AdSense (ou outro). Mobile ignora. */
18
+ renderWebSlot?: (props: NativeAdWebSlotProps) => ReactElement | null;
9
19
  };
10
20
 
11
21
  let nativeAdCatalog: NativeAdCatalog | null = null;
@@ -1,6 +1,41 @@
1
+ import {
2
+ DEFAULT_NATIVE_AD_ASPECT_RATIO,
3
+ DEFAULT_NATIVE_AD_PLACEMENT,
4
+ getNativeAdCatalog,
5
+ } from '../ads/native-ad-catalog';
1
6
  import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
2
7
 
3
- /** Web stub — native ads require a native build. */
4
- export function SduiNativeAdBlock(_props: SduiBlockRenderProps) {
5
- return null;
8
+ function resolveBlockWidth(props: Record<string, unknown>): number | undefined {
9
+ if (typeof props.width === 'number' && props.width > 0) return props.width;
10
+ return undefined;
11
+ }
12
+
13
+ function resolvePlacement(props: Record<string, unknown>): string {
14
+ return typeof props.placement === 'string' && props.placement.length > 0
15
+ ? props.placement
16
+ : DEFAULT_NATIVE_AD_PLACEMENT;
17
+ }
18
+
19
+ function resolveAspectRatio(placement: string, props: Record<string, unknown>): number {
20
+ if (typeof props.aspect_ratio === 'number' && props.aspect_ratio > 0) {
21
+ return props.aspect_ratio;
22
+ }
23
+
24
+ const catalog = getNativeAdCatalog();
25
+ const placementConfig = catalog?.getPlacementConfig?.(placement);
26
+ return placementConfig?.aspectRatio ?? DEFAULT_NATIVE_AD_ASPECT_RATIO;
27
+ }
28
+
29
+ /** Web — AdSense via `catalog.renderWebSlot` (configurado no app). */
30
+ export function SduiNativeAdBlock({ block }: SduiBlockRenderProps) {
31
+ const catalog = getNativeAdCatalog();
32
+ if (!catalog?.isEnabled() || !catalog.renderWebSlot) {
33
+ return null;
34
+ }
35
+
36
+ const placement = resolvePlacement(block.props);
37
+ const width = resolveBlockWidth(block.props);
38
+ const aspectRatio = resolveAspectRatio(placement, block.props);
39
+
40
+ return catalog.renderWebSlot({ placement, width, aspectRatio });
6
41
  }
@@ -27,6 +27,9 @@ export type {
27
27
  SduiNativeBlockRenderProps,
28
28
  } from './sdui-hybrid-renderer-context';
29
29
  export * from './theme/proulr-theme';
30
+ export * from './theme/chrome-glass';
31
+ export * from './theme/chrome-layout';
32
+ export * from './theme/chrome-pill-styles';
30
33
  export { resolveViewStyleProps, resolveTextStyleProps } from './theme/resolve-style-props';
31
34
  export {
32
35
  formatDistanceLabel,
@@ -47,6 +50,10 @@ export {
47
50
  DEFAULT_NATIVE_AD_PLACEMENT,
48
51
  DEFAULT_NATIVE_AD_ASPECT_RATIO,
49
52
  } from './ads/native-ad-catalog';
50
- export type { NativeAdCatalog, NativeAdPlacementConfig } from './ads/native-ad-catalog';
53
+ export type {
54
+ NativeAdCatalog,
55
+ NativeAdPlacementConfig,
56
+ NativeAdWebSlotProps,
57
+ } from './ads/native-ad-catalog';
51
58
  export * from './ui';
52
59
  export { brToISO, isValidDateBR, maskDateBR } from './lib/brazilian-date-input';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Chrome glass (BlurView) — fonte única Night Stalker.
3
+ * Overlay de cor fica em `colors.chromeGlassOverlay` (tema).
4
+ */
5
+
6
+ /** Intensidade do frost — mais blur, menos “placa” opaca. */
7
+ export const CHROME_GLASS_BLUR_INTENSITY = 50;
8
+
9
+ /**
10
+ * Tint limpo (sem material cinza do system material).
11
+ * Tipado como string literal para não exigir peer `expo-blur` na lib.
12
+ */
13
+ export const CHROME_GLASS_BLUR_TINT = 'dark' as const;
14
+
15
+ export type ChromeGlassBlurTint = typeof CHROME_GLASS_BLUR_TINT;
@@ -0,0 +1,42 @@
1
+ import { m3Shape, spacing } from './proulr-theme';
2
+
3
+ export const CHROME_HORIZONTAL_INSET = spacing.md;
4
+ export const CHROME_VERTICAL_GAP = spacing.sm;
5
+ export const CHROME_PILL_RADIUS = m3Shape.cornerFull;
6
+ export const HEADER_PILL_ROW_HEIGHT = 56;
7
+ export const HEADER_SLOT_GAP = spacing.md;
8
+ export const HEADER_SLOT_ROW_HEIGHT = 52;
9
+ export const FLOATING_TAB_BAR_HEIGHT = 60;
10
+ export const CHROME_CONTENT_TOP_GAP = spacing.sm;
11
+ export const CHROME_SCROLL_BOTTOM_EXTRA = spacing.sm;
12
+ export const CHROME_SCROLL_FADE_EXTRA = 32;
13
+ export const CHROME_SCROLL_FADE_Z_BEHIND_CHROME = 1;
14
+ export const CHROME_SCROLL_FADE_Z_ABOVE_CONTENT = 5;
15
+ export const CHROME_PILL_Z_INDEX = 10;
16
+ export const CHROME_DRAWER_OVERLAY_Z_INDEX = 30;
17
+ export const CHROME_DRAWER_Z_INDEX = 31;
18
+
19
+ type HeaderChromeOptions = {
20
+ hasSlot?: boolean;
21
+ searchOpen?: boolean;
22
+ };
23
+
24
+ export function estimateHeaderChromeHeight(
25
+ topInset: number,
26
+ options: boolean | HeaderChromeOptions = false,
27
+ ): number {
28
+ const { hasSlot = false, searchOpen = false } =
29
+ typeof options === 'boolean' ? { hasSlot: options } : options;
30
+ const showsSlot = hasSlot && !searchOpen;
31
+ const slotRow = showsSlot ? HEADER_SLOT_GAP + HEADER_SLOT_ROW_HEIGHT : 0;
32
+ return topInset + CHROME_VERTICAL_GAP + HEADER_PILL_ROW_HEIGHT + slotRow;
33
+ }
34
+
35
+ export function estimateTabBarChromeHeight(bottomInset: number): number {
36
+ return bottomInset + CHROME_VERTICAL_GAP + FLOATING_TAB_BAR_HEIGHT;
37
+ }
38
+
39
+ /** Mesma área reservada que a tab bar — footer flutuante com CTA primário. */
40
+ export function estimateChromeFooterActionHeight(bottomInset: number): number {
41
+ return estimateTabBarChromeHeight(bottomInset);
42
+ }
@@ -0,0 +1,13 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { colors } from './proulr-theme';
3
+ import { CHROME_PILL_RADIUS } from './chrome-layout';
4
+
5
+ export const chromePillStyles = StyleSheet.create({
6
+ shell: {
7
+ backgroundColor: colors.chromePillBackground,
8
+ borderWidth: 1,
9
+ borderColor: colors.chromePillBorder,
10
+ borderRadius: CHROME_PILL_RADIUS,
11
+ overflow: 'hidden',
12
+ },
13
+ });
@@ -0,0 +1,4 @@
1
+ export * from './proulr-theme';
2
+ export * from './chrome-glass';
3
+ export * from './chrome-layout';
4
+ export * from './chrome-pill-styles';
@@ -1,6 +1,6 @@
1
- import { Platform, ViewStyle } from 'react-native';
1
+ import { Platform, type ViewStyle } from 'react-native';
2
2
 
3
- /** Proulr UI theme — Night Stalker OLED. */
3
+ /** Proulr UI theme — Night Stalker OLED (fonte única para app + SDUI). */
4
4
 
5
5
  export const m3Colors = {
6
6
  primary: '#D946EF',
@@ -30,9 +30,8 @@ export const m3Colors = {
30
30
  inverseSurface: '#FFFFFF',
31
31
  inverseOnSurface: '#000000',
32
32
  scrim: 'rgba(0,0,0,0.78)',
33
+ storyInteractionScrim: 'rgba(0,0,0,0.48)',
33
34
  online: '#34FF88',
34
- chromePillBackground: 'rgba(28, 28, 30, 0.82)',
35
- chromePillBorder: 'rgba(255, 255, 255, 0.06)',
36
35
  } as const;
37
36
 
38
37
  export const m3Shape = {
@@ -79,35 +78,58 @@ export const colors = {
79
78
  dangerSoft: 'rgba(255,68,68,0.22)',
80
79
  overlay: 'rgba(0,0,0,0.78)',
81
80
  surfaceGlass: 'rgba(255,255,255,0.06)',
82
- chromePillBackground: 'rgba(28, 28, 30, 0.82)',
83
- chromePillBorder: 'rgba(255, 255, 255, 0.06)',
84
- primary: m3Colors.primary,
85
- onSurface: m3Colors.onSurface,
86
- onSurfaceVariant: m3Colors.onSurfaceVariant,
87
- };
81
+ /** Pílula / chrome sem BlurView — preto OLED. */
82
+ chromePillBackground: 'rgba(0, 0, 0, 0.82)',
83
+ /** Overlay sobre BlurView — transparente = frost limpo (sem placa acinzentada). */
84
+ chromeGlassOverlay: 'rgba(0, 0, 0, 0)',
85
+ chromePillBorder: 'rgba(255, 255, 255, 0.08)',
86
+ videoControlBackground: 'rgba(0,0,0,0.45)',
87
+ videoTrackBackground: 'rgba(255,255,255,0.25)',
88
+ scrollbarThumb: 'rgba(255, 255, 255, 0.28)',
89
+ scrollbarThumbHover: 'rgba(255, 255, 255, 0.45)',
90
+ primary: '#D946EF',
91
+ onSurface: '#FFFFFF',
92
+ onSurfaceVariant: '#A3A3A3',
93
+ } as const;
88
94
 
89
- export const radius = {
95
+ export const spacing = {
96
+ xs: 4,
90
97
  sm: 8,
91
98
  md: 12,
92
99
  lg: 16,
93
100
  xl: 24,
94
- pill: 999,
101
+ xxl: 32,
95
102
  };
96
103
 
97
- export const spacing = {
98
- xs: 4,
104
+ export const radius = {
99
105
  sm: 8,
100
106
  md: 12,
101
107
  lg: 16,
102
108
  xl: 24,
103
- xxl: 32,
104
- } as const;
109
+ pill: 999,
110
+ };
105
111
 
106
112
  export const gradients = {
107
113
  brand: ['#7A0D8A', '#D946EF'] as const,
108
114
  header: ['#2D0A35', '#D946EF'] as const,
109
115
  headerGlow: ['rgba(217,70,239,0.20)', 'rgba(217,70,239,0)'] as const,
116
+ authHero: ['rgba(217,70,239,0.14)', 'rgba(0,229,255,0.06)', 'transparent'] as const,
110
117
  neonBorder: [colors.headerEnd, colors.accent] as const,
118
+ videoControlsShade: ['transparent', 'rgba(0,0,0,0.72)'] as const,
119
+ chromeScrollFadeBottom: [
120
+ 'transparent',
121
+ 'rgba(0,0,0,0.22)',
122
+ 'rgba(0,0,0,0.65)',
123
+ '#000000',
124
+ ] as const,
125
+ chromeScrollFadeBottomLocations: [0, 0.45, 0.82, 1] as const,
126
+ chromeScrollFadeTop: [
127
+ '#000000',
128
+ 'rgba(0,0,0,0.65)',
129
+ 'rgba(0,0,0,0.22)',
130
+ 'transparent',
131
+ ] as const,
132
+ chromeScrollFadeTopLocations: [0, 0.18, 0.55, 1] as const,
111
133
  };
112
134
 
113
135
  export const fonts = {
@@ -116,7 +138,7 @@ export const fonts = {
116
138
  semibold: 'Montserrat_600SemiBold',
117
139
  bold: 'Montserrat_700Bold',
118
140
  extrabold: 'Montserrat_800ExtraBold',
119
- };
141
+ } as const;
120
142
 
121
143
  export const shadows = {
122
144
  card: Platform.select<ViewStyle>({
@@ -0,0 +1,78 @@
1
+ import React, { type ReactNode } from 'react';
2
+ import { BlurView } from 'expo-blur';
3
+ import {
4
+ StyleSheet,
5
+ View,
6
+ type StyleProp,
7
+ type ViewProps,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+ import { colors } from '../theme/proulr-theme';
11
+ import {
12
+ CHROME_GLASS_BLUR_INTENSITY,
13
+ CHROME_GLASS_BLUR_TINT,
14
+ } from '../theme/chrome-glass';
15
+ import { chromePillStyles } from '../theme/chrome-pill-styles';
16
+
17
+ const absoluteFill: ViewStyle = {
18
+ position: 'absolute',
19
+ top: 0,
20
+ right: 0,
21
+ bottom: 0,
22
+ left: 0,
23
+ };
24
+
25
+ type ChromeGlassLayerProps = {
26
+ style?: StyleProp<ViewStyle>;
27
+ pointerEvents?: ViewProps['pointerEvents'];
28
+ };
29
+
30
+ /**
31
+ * Camada de frost única do sistema (igual AppSideDrawer topo/base).
32
+ * BlurView + `colors.chromeGlassOverlay` — sem cast cinza.
33
+ */
34
+ export function ChromeGlassLayer({
35
+ style,
36
+ pointerEvents = 'none',
37
+ }: ChromeGlassLayerProps) {
38
+ return (
39
+ <View style={[absoluteFill, style]} pointerEvents={pointerEvents}>
40
+ <BlurView
41
+ intensity={CHROME_GLASS_BLUR_INTENSITY}
42
+ tint={CHROME_GLASS_BLUR_TINT}
43
+ style={StyleSheet.absoluteFill}
44
+ />
45
+ <View style={[absoluteFill, styles.tint]} />
46
+ </View>
47
+ );
48
+ }
49
+
50
+ type ChromeGlassShellProps = {
51
+ children: ReactNode;
52
+ style?: StyleProp<ViewStyle>;
53
+ contentStyle?: StyleProp<ViewStyle>;
54
+ };
55
+
56
+ /**
57
+ * Pílula chrome com o mesmo glass do drawer (shell + blur + overlay).
58
+ */
59
+ export function ChromeGlassShell({ children, style, contentStyle }: ChromeGlassShellProps) {
60
+ return (
61
+ <View style={[chromePillStyles.shell, styles.shellTransparent, style]}>
62
+ <ChromeGlassLayer />
63
+ <View style={[styles.content, contentStyle]}>{children}</View>
64
+ </View>
65
+ );
66
+ }
67
+
68
+ const styles = StyleSheet.create({
69
+ tint: {
70
+ backgroundColor: colors.chromeGlassOverlay,
71
+ },
72
+ shellTransparent: {
73
+ backgroundColor: 'transparent',
74
+ },
75
+ content: {
76
+ zIndex: 1,
77
+ },
78
+ });
@@ -15,6 +15,7 @@ import {
15
15
  type ViewStyle,
16
16
  } from 'react-native';
17
17
  import { AppIcon } from './app-icon';
18
+ import { ChromeGlassLayer } from './ChromeGlass';
18
19
  import { colors, fonts, m3Colors, m3Shape, spacing } from '../theme/proulr-theme';
19
20
 
20
21
  const ACTION_BUTTON_SIZE = 48;
@@ -241,9 +242,11 @@ function composePillVariantColors(variant: ComposePillVariant) {
241
242
  return {
242
243
  text: colors.white,
243
244
  placeholder: 'rgba(255,255,255,0.55)',
244
- pillBackground: 'rgba(28,28,30,0.72)',
245
- pillBorder: 'rgba(255,255,255,0.08)',
246
- replyBackground: 'rgba(28,28,30,0.72)',
245
+ /** Transparent — frost via ChromeGlassLayer (mesmo glass do drawer). */
246
+ pillBackground: 'transparent',
247
+ pillBorder: colors.chromePillBorder,
248
+ replyBackground: 'transparent',
249
+ useChromeGlass: true,
247
250
  };
248
251
  }
249
252
 
@@ -251,9 +254,10 @@ function composePillVariantColors(variant: ComposePillVariant) {
251
254
  return {
252
255
  text: m3Colors.onSurface,
253
256
  placeholder: m3Colors.onSurfaceVariant,
254
- pillBackground: colors.chromePillBackground,
257
+ pillBackground: 'transparent',
255
258
  pillBorder: colors.chromePillBorder,
256
- replyBackground: colors.chromePillBackground,
259
+ replyBackground: 'transparent',
260
+ useChromeGlass: true,
257
261
  };
258
262
  }
259
263
 
@@ -263,6 +267,7 @@ function composePillVariantColors(variant: ComposePillVariant) {
263
267
  pillBackground: m3Colors.surfaceContainerHigh,
264
268
  pillBorder: 'transparent',
265
269
  replyBackground: m3Colors.surfaceContainerHigh,
270
+ useChromeGlass: false,
266
271
  };
267
272
  }
268
273
 
@@ -276,7 +281,14 @@ export function ComposePillReplyBar({
276
281
  const variantColors = composePillVariantColors(variant);
277
282
 
278
283
  return (
279
- <View style={[styles.replyBar, { backgroundColor: variantColors.replyBackground }]}>
284
+ <View
285
+ style={[
286
+ styles.replyBar,
287
+ { backgroundColor: variantColors.replyBackground },
288
+ variantColors.useChromeGlass && styles.replyBarGlass,
289
+ ]}
290
+ >
291
+ {variantColors.useChromeGlass ? <ChromeGlassLayer /> : null}
280
292
  <View style={styles.replyBarAccent} />
281
293
  <View style={styles.replyBarContent}>
282
294
  <Text style={styles.replyBarName} numberOfLines={1}>
@@ -427,8 +439,10 @@ export function ComposePillInput({
427
439
  borderColor: variantColors.pillBorder,
428
440
  borderWidth: variant === 'overlay' || variant === 'chrome' ? 1 : 0,
429
441
  },
442
+ variantColors.useChromeGlass && styles.inputPillGlass,
430
443
  ]}
431
444
  >
445
+ {variantColors.useChromeGlass ? <ChromeGlassLayer /> : null}
432
446
  {leading}
433
447
  {textField}
434
448
  {trailing ? (
@@ -466,15 +480,20 @@ const styles = StyleSheet.create({
466
480
  borderRadius: m3Shape.cornerMedium,
467
481
  gap: spacing.sm,
468
482
  },
483
+ replyBarGlass: {
484
+ overflow: 'hidden',
485
+ },
469
486
  replyBarAccent: {
470
487
  width: 4,
471
488
  alignSelf: 'stretch',
472
489
  borderRadius: m3Shape.cornerExtraSmall,
473
490
  backgroundColor: m3Colors.primary,
491
+ zIndex: 1,
474
492
  },
475
493
  replyBarContent: {
476
494
  flex: 1,
477
495
  gap: 2,
496
+ zIndex: 1,
478
497
  },
479
498
  replyBarName: {
480
499
  color: m3Colors.primary,
@@ -491,6 +510,7 @@ const styles = StyleSheet.create({
491
510
  height: 32,
492
511
  alignItems: 'center',
493
512
  justifyContent: 'center',
513
+ zIndex: 1,
494
514
  },
495
515
  composerRow: {
496
516
  flexDirection: 'row',
@@ -506,6 +526,7 @@ const styles = StyleSheet.create({
506
526
  paddingRight: spacing.xs,
507
527
  overflow: 'hidden',
508
528
  },
529
+ inputPillGlass: {},
509
530
  inputPillSingle: {
510
531
  borderRadius: m3Shape.cornerFull,
511
532
  },
@@ -517,6 +538,7 @@ const styles = StyleSheet.create({
517
538
  height: 40,
518
539
  alignItems: 'center',
519
540
  justifyContent: 'center',
541
+ zIndex: 1,
520
542
  },
521
543
  iconButtonDisabled: {
522
544
  opacity: 0.4,
@@ -524,6 +546,7 @@ const styles = StyleSheet.create({
524
546
  trailingIcons: {
525
547
  flexDirection: 'row',
526
548
  alignItems: 'center',
549
+ zIndex: 1,
527
550
  },
528
551
  trailingIconsExpanded: {
529
552
  paddingBottom: spacing.xs,
@@ -536,6 +559,7 @@ const styles = StyleSheet.create({
536
559
  paddingHorizontal: spacing.xs,
537
560
  paddingTop: VERTICAL_PAD,
538
561
  paddingBottom: VERTICAL_PAD,
562
+ zIndex: 1,
539
563
  },
540
564
  bareInput: {
541
565
  minHeight: 40,
@@ -4,6 +4,7 @@ export { AppLoadingSpinner } from './AppLoadingSpinner';
4
4
  export { AuthStepShell } from './AuthStepShell';
5
5
  export { Card } from './Card';
6
6
  export { Chip } from './Chip';
7
+ export { ChromeGlassLayer, ChromeGlassShell } from './ChromeGlass';
7
8
  export { ComposeAudiencePill } from './compose-audience-pill';
8
9
  export {
9
10
  ComposeCameraGlyph,