@proulr/sdui-components 0.1.51 → 0.1.54

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.51",
3
+ "version": "0.1.54",
4
4
  "type": "module",
5
5
  "main": "./src/renderer/index.ts",
6
6
  "types": "./src/renderer/index.ts",
@@ -6,12 +6,13 @@ import {
6
6
  DiscoverMap,
7
7
  DiscoverProfileCard,
8
8
  } from './discover';
9
- import { TextField } from './forms';
9
+ import { SelectRow, TextField } from './forms';
10
10
  import {
11
11
  ChatInboxCapabilities,
12
12
  EmptyState,
13
13
  FlashInboxCapabilities,
14
14
  FloatingCreateMenu,
15
+ InboxHubChrome,
15
16
  InboxPersonRow,
16
17
  InboxSearchField,
17
18
  ProfileVisitsCapabilities,
@@ -85,6 +86,7 @@ describe('bdc discover/forms/social/inbox/native', () => {
85
86
  expect(DiscoverProfileCard({ profileRef: 'p' }).type).toBe('discover_profile_card');
86
87
  expect(DiscoverMap({}).type).toBe('discover_map');
87
88
  expect(TextField({ placeholder: 'x' }).type).toBe('text_field');
89
+ expect(SelectRow({ label: 'Pick', optionsRef: 'form_options.gender' }).type).toBe('select_row');
88
90
  expect(StoryRail({ surface: 'discover' }).type).toBe('story_rail');
89
91
  expect(ProfileCompletionBanner({}).type).toBe('profile_completion_banner');
90
92
  });
@@ -97,6 +99,11 @@ describe('bdc discover/forms/social/inbox/native', () => {
97
99
  expect(ChatInboxCapabilities({}).type).toBe('chat_inbox_capabilities');
98
100
  expect(FlashInboxCapabilities({}).type).toBe('flash_inbox_capabilities');
99
101
  expect(ProfileVisitsCapabilities({}).type).toBe('profile_visits_capabilities');
102
+ expect(InboxHubChrome({}).type).toBe('inbox_hub_chrome');
103
+ expect(InboxHubChrome({}).props).toMatchObject({
104
+ hub_ref: 'hub',
105
+ active_filter_ref: 'active_filter',
106
+ });
100
107
  const menu = FloatingCreateMenu({
101
108
  id: 'fab',
102
109
  closedLabel: '+',
@@ -18,3 +18,13 @@ type TextFieldProps = LayoutProps & {
18
18
  };
19
19
 
20
20
  export const TextField: BdcComponent = (props) => layoutBlock('text_field', props as TextFieldProps);
21
+
22
+ type SelectRowProps = LayoutProps & {
23
+ label?: string;
24
+ dataRef?: string;
25
+ optionsRef?: string;
26
+ changeActionId?: string;
27
+ disabled?: boolean;
28
+ };
29
+
30
+ export const SelectRow: BdcComponent = (props) => layoutBlock('select_row', props as SelectRowProps);
@@ -81,6 +81,26 @@ type FloatingCreateMenuProps = {
81
81
  actions: FloatingCreateMenuActionProps[];
82
82
  };
83
83
 
84
+ type InboxHubChromeProps = {
85
+ id?: string;
86
+ hubRef?: string;
87
+ activeFilterRef?: string;
88
+ };
89
+
90
+ /**
91
+ * Overlay do seletor/filtros do hub inbox (sibling do Scroll).
92
+ * Hybrid nativo: collapse + badges realtime; actions via onAction.
93
+ */
94
+ export const InboxHubChrome: BdcComponent = (props) =>
95
+ leafBlock('inbox_hub_chrome', {
96
+ ...(typeof (props as InboxHubChromeProps).id === 'string'
97
+ ? { id: (props as InboxHubChromeProps).id }
98
+ : {}),
99
+ hub_ref: (props as InboxHubChromeProps).hubRef ?? 'hub',
100
+ active_filter_ref:
101
+ (props as InboxHubChromeProps).activeFilterRef ?? 'active_filter',
102
+ });
103
+
84
104
  /** Overlay FAB “+” (sibling do Scroll) — ações via action_id do wire. */
85
105
  export const FloatingCreateMenu: BdcComponent = (props) => {
86
106
  const menu = props as FloatingCreateMenuProps;
package/src/bdc/index.ts CHANGED
@@ -23,13 +23,14 @@ export {
23
23
  EmptyState,
24
24
  FlashInboxCapabilities,
25
25
  FloatingCreateMenu,
26
+ InboxHubChrome,
26
27
  InboxPersonRow,
27
28
  InboxSearchField,
28
29
  ProfileVisitsCapabilities,
29
30
  SegmentedTabs,
30
31
  } from './components/inbox';
31
32
  export type { FloatingCreateMenuActionProps } from './components/inbox';
32
- export { TextField } from './components/forms';
33
+ export { TextField, SelectRow } from './components/forms';
33
34
  export { ProfileCompletionBanner, StoryRail } from './components/social';
34
35
  export {
35
36
  AlbumDetail,
@@ -9,6 +9,8 @@ export const SDUI_BLOCK_TYPES = [
9
9
  'spacer',
10
10
  'info_banner',
11
11
  'native_ad',
12
+ 'inbox_hub_chrome',
13
+ 'select_row',
12
14
  ] as const;
13
15
 
14
16
  export type SduiBlockType = (typeof SDUI_BLOCK_TYPES)[number];
@@ -32,7 +32,9 @@ import {
32
32
  describe('block-types', () => {
33
33
  it('lists known block types', () => {
34
34
  expect(SDUI_BLOCK_TYPES).toContain('text');
35
+ expect(SDUI_BLOCK_TYPES).toContain('inbox_hub_chrome');
35
36
  expect(isKnownBlockType('button')).toBe(true);
37
+ expect(isKnownBlockType('inbox_hub_chrome')).toBe(true);
36
38
  expect(isKnownBlockType('unknown_block')).toBe(false);
37
39
  });
38
40
  });
@@ -11,7 +11,7 @@ type Props = {
11
11
  };
12
12
 
13
13
  /** Blocos overlay (FAB etc.) — não competem por flex com o Scroll. */
14
- const OVERLAY_BLOCK_TYPES = new Set(['floating_create_menu']);
14
+ const OVERLAY_BLOCK_TYPES = new Set(['floating_create_menu', 'inbox_hub_chrome']);
15
15
 
16
16
  export function SduiScreenRenderer({ screen, onAction }: Props) {
17
17
  return (
@@ -227,6 +227,20 @@ export function SduiFloatingCreateMenuBlock(props: SduiBlockRenderProps) {
227
227
  return <>{hybrid.renderFloatingCreateMenu(props)}</>;
228
228
  }
229
229
 
230
+ export function SduiInboxHubChromeBlock(props: SduiBlockRenderProps) {
231
+ const hybrid = useSduiHybridRenderer();
232
+ if (typeof hybrid.renderInboxHubChrome !== 'function') return null;
233
+ return (
234
+ <>
235
+ {hybrid.renderInboxHubChrome({
236
+ ...props,
237
+ hubRef: readStringProp(props.block, 'hub_ref') ?? 'hub',
238
+ activeFilterRef: readStringProp(props.block, 'active_filter_ref') ?? 'active_filter',
239
+ })}
240
+ </>
241
+ );
242
+ }
243
+
230
244
  const styles = StyleSheet.create({
231
245
  segmentTrack: {
232
246
  flexDirection: 'row',
@@ -5,9 +5,11 @@ import { Card } from '../ui/Card';
5
5
  import { AppIcon } from '../ui/app-icon';
6
6
  import { colors, fonts, m3Colors, m3Type, spacing } from '../theme/proulr-theme';
7
7
  import {
8
+ resolveDataRef,
8
9
  resolveDataRefBoolean,
9
10
  resolveDataRefString,
10
11
  } from '../lib/resolve-data-ref';
12
+ import { SelectRow as UiSelectRow } from '../ui/SelectRow';
11
13
  import { resolveViewStyleProps } from '../theme/resolve-style-props';
12
14
  import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks';
13
15
 
@@ -117,6 +119,29 @@ export function SduiToggleRowBlock({ block, data, onAction }: SduiBlockRenderPro
117
119
  );
118
120
  }
119
121
 
122
+ export function SduiSelectRowBlock({ block, data, onAction }: SduiBlockRenderProps) {
123
+ const label = readStringProp(block, 'label') ?? '';
124
+ const dataRef = readStringProp(block, 'data_ref');
125
+ const optionsRef = readStringProp(block, 'options_ref');
126
+ const changeActionId = readStringProp(block, 'change_action_id');
127
+ const disabled = readBooleanProp(block, 'disabled');
128
+ const value = dataRef ? resolveDataRefString(data, dataRef) : '';
129
+ const optionsRaw = optionsRef ? resolveDataRef(data, optionsRef) : [];
130
+ const options = Array.isArray(optionsRaw) ? optionsRaw.map(String) : [];
131
+
132
+ return (
133
+ <UiSelectRow
134
+ label={label}
135
+ value={value || null}
136
+ options={options}
137
+ onChange={(next) => {
138
+ if (disabled || !changeActionId) return;
139
+ onAction(changeActionId, { value: next });
140
+ }}
141
+ />
142
+ );
143
+ }
144
+
120
145
  const styles = StyleSheet.create({
121
146
  card: {
122
147
  paddingVertical: spacing.xs,
@@ -12,11 +12,13 @@ export {
12
12
  export {
13
13
  SduiChromeScrollProvider,
14
14
  useSduiChromeScroll,
15
+ useSduiChromeScrollOverlayRegistry,
15
16
  } from './sdui-chrome-scroll-context';
16
17
  export type {
17
18
  SduiChromeScrollConfig,
18
19
  SduiChromeScrollInsetProps,
19
20
  SduiChromeScrollInsets,
21
+ SduiChromeScrollOverlayContribution,
20
22
  } from './sdui-chrome-scroll-context';
21
23
  export type {
22
24
  SduiHybridDiscoverMapProps,
@@ -1,9 +1,18 @@
1
- import React, { createContext, useContext, type ReactElement, type ReactNode } from 'react';
2
- import type {
3
- NativeScrollEvent,
4
- NativeSyntheticEvent,
5
- RefreshControlProps,
6
- ViewStyle,
1
+ import React, {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useMemo,
6
+ useState,
7
+ type ReactElement,
8
+ type ReactNode,
9
+ } from 'react';
10
+ import {
11
+ Platform,
12
+ type NativeScrollEvent,
13
+ type NativeSyntheticEvent,
14
+ type RefreshControlProps,
15
+ type ViewStyle,
7
16
  } from 'react-native';
8
17
 
9
18
  export type SduiChromeScrollInsetProps = {
@@ -31,7 +40,74 @@ export type SduiChromeScrollConfig = {
31
40
  scrollViewProps?: Record<string, unknown>;
32
41
  };
33
42
 
43
+ export type SduiChromeScrollOverlayContribution = {
44
+ /** Altura reservada abaixo do AppHeader (ex.: seletor + filtros do inbox). */
45
+ extraTopInset?: number;
46
+ onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
47
+ };
48
+
49
+ type OverlayRegistry = {
50
+ register: (id: string, contribution: SduiChromeScrollOverlayContribution) => () => void;
51
+ };
52
+
34
53
  const SduiChromeScrollContext = createContext<SduiChromeScrollConfig | null>(null);
54
+ const SduiChromeScrollOverlayRegistryContext = createContext<OverlayRegistry | null>(null);
55
+
56
+ function mergeChromeScrollConfig(
57
+ base: SduiChromeScrollConfig,
58
+ overlays: Map<string, SduiChromeScrollOverlayContribution>,
59
+ ): SduiChromeScrollConfig {
60
+ let extraTop = 0;
61
+ const scrollHandlers: ((event: NativeSyntheticEvent<NativeScrollEvent>) => void)[] = [];
62
+ if (base.onScroll) scrollHandlers.push(base.onScroll);
63
+
64
+ for (const contribution of overlays.values()) {
65
+ if (typeof contribution.extraTopInset === 'number' && contribution.extraTopInset > 0) {
66
+ extraTop = Math.max(extraTop, contribution.extraTopInset);
67
+ }
68
+ if (contribution.onScroll) scrollHandlers.push(contribution.onScroll);
69
+ }
70
+
71
+ if (extraTop <= 0 && scrollHandlers.length <= (base.onScroll ? 1 : 0)) {
72
+ return base;
73
+ }
74
+
75
+ const contentContainerStyle: ViewStyle = { ...base.contentContainerStyle };
76
+ let scrollInsetProps: SduiChromeScrollInsetProps = { ...base.scrollInsetProps };
77
+
78
+ if (extraTop > 0) {
79
+ // Paridade com buildSduiChromeScrollConfig(extraTopInset): iOS reserva header via contentInset.
80
+ const headerTop = base.chromeInsets.top;
81
+ const insetTop = headerTop + extraTop;
82
+ contentContainerStyle.paddingTop = Platform.OS === 'ios' ? extraTop : insetTop;
83
+ if (Platform.OS === 'ios') {
84
+ scrollInsetProps = {
85
+ contentInset: {
86
+ top: insetTop,
87
+ bottom: base.chromeInsets.bottom,
88
+ },
89
+ contentOffset: { x: 0, y: -insetTop },
90
+ automaticallyAdjustContentInsets: false,
91
+ scrollIndicatorInsets: {
92
+ top: insetTop,
93
+ bottom: base.chromeInsets.bottom,
94
+ },
95
+ };
96
+ }
97
+ }
98
+
99
+ return {
100
+ ...base,
101
+ contentContainerStyle,
102
+ scrollInsetProps,
103
+ onScroll:
104
+ scrollHandlers.length > 0
105
+ ? (event) => {
106
+ for (const handler of scrollHandlers) handler(event);
107
+ }
108
+ : undefined,
109
+ };
110
+ }
35
111
 
36
112
  export function SduiChromeScrollProvider({
37
113
  value,
@@ -40,11 +116,43 @@ export function SduiChromeScrollProvider({
40
116
  value: SduiChromeScrollConfig;
41
117
  children: ReactNode;
42
118
  }) {
119
+ const [overlays, setOverlays] = useState(
120
+ () => new Map<string, SduiChromeScrollOverlayContribution>(),
121
+ );
122
+
123
+ const register = useCallback((id: string, contribution: SduiChromeScrollOverlayContribution) => {
124
+ setOverlays((prev) => {
125
+ const next = new Map(prev);
126
+ next.set(id, contribution);
127
+ return next;
128
+ });
129
+ return () => {
130
+ setOverlays((prev) => {
131
+ if (!prev.has(id)) return prev;
132
+ const next = new Map(prev);
133
+ next.delete(id);
134
+ return next;
135
+ });
136
+ };
137
+ }, []);
138
+
139
+ const registry = useMemo<OverlayRegistry>(() => ({ register }), [register]);
140
+ const merged = useMemo(() => mergeChromeScrollConfig(value, overlays), [overlays, value]);
141
+
43
142
  return (
44
- <SduiChromeScrollContext.Provider value={value}>{children}</SduiChromeScrollContext.Provider>
143
+ <SduiChromeScrollOverlayRegistryContext.Provider value={registry}>
144
+ <SduiChromeScrollContext.Provider value={merged}>{children}</SduiChromeScrollContext.Provider>
145
+ </SduiChromeScrollOverlayRegistryContext.Provider>
45
146
  );
46
147
  }
47
148
 
48
149
  export function useSduiChromeScroll(): SduiChromeScrollConfig | null {
49
150
  return useContext(SduiChromeScrollContext);
50
151
  }
152
+
153
+ /**
154
+ * Hybrids de overlay (ex.: inbox_hub_chrome) registram inset/onScroll sem especializar o shell.
155
+ */
156
+ export function useSduiChromeScrollOverlayRegistry(): OverlayRegistry | null {
157
+ return useContext(SduiChromeScrollOverlayRegistryContext);
158
+ }
@@ -16,6 +16,7 @@ import {
16
16
  SduiCardBlock,
17
17
  SduiListItemBlock,
18
18
  SduiSectionHeaderBlock,
19
+ SduiSelectRowBlock,
19
20
  SduiToggleRowBlock,
20
21
  } from './blocks/SduiLayoutBlocks';
21
22
  import {
@@ -38,6 +39,7 @@ import {
38
39
  SduiInboxSearchFieldBlock,
39
40
  SduiProfileVisitsCapabilitiesBlock,
40
41
  SduiFloatingCreateMenuBlock,
42
+ SduiInboxHubChromeBlock,
41
43
  SduiSegmentedTabsBlock,
42
44
  } from './blocks/SduiInboxBlocks';
43
45
  import { SduiNativeAdBlock } from './blocks/SduiNativeAdBlock';
@@ -85,6 +87,7 @@ const REGISTRY: Record<string, SduiBlockComponent> = {
85
87
  section_header: SduiSectionHeaderBlock,
86
88
  list_item: SduiListItemBlock,
87
89
  toggle_row: SduiToggleRowBlock,
90
+ select_row: SduiSelectRowBlock,
88
91
  chip: SduiChipBlock,
89
92
  chip_row: SduiChipRowBlock,
90
93
  segmented_tabs: SduiSegmentedTabsBlock,
@@ -95,6 +98,7 @@ const REGISTRY: Record<string, SduiBlockComponent> = {
95
98
  flash_inbox_capabilities: SduiFlashInboxCapabilitiesBlock,
96
99
  profile_visits_capabilities: SduiProfileVisitsCapabilitiesBlock,
97
100
  floating_create_menu: SduiFloatingCreateMenuBlock,
101
+ inbox_hub_chrome: SduiInboxHubChromeBlock,
98
102
  discover_grid: SduiDiscoverGridBlock,
99
103
  discover_profile_card: SduiDiscoverProfileCardBlock,
100
104
  discover_map: SduiDiscoverMapBlock,
@@ -105,6 +105,17 @@ export type SduiHybridRenderers = {
105
105
  renderFlashInboxCapabilities?: (props: SduiNativeBlockRenderProps) => ReactNode;
106
106
  renderProfileVisitsCapabilities?: (props: SduiNativeBlockRenderProps) => ReactNode;
107
107
  renderFloatingCreateMenu?: (props: SduiNativeBlockRenderProps) => ReactNode;
108
+ renderInboxHubChrome?: (
109
+ props: SduiNativeBlockRenderProps & {
110
+ hubRef?: string;
111
+ activeFilterRef?: string;
112
+ },
113
+ ) => ReactNode;
114
+ /**
115
+ * Optional host wrap for screen body (ex.: InboxHubSduiProvider).
116
+ * Keeps domain providers out of the generic shell.
117
+ */
118
+ wrapScreenBody?: (children: ReactNode) => ReactNode;
108
119
  };
109
120
 
110
121
  const SduiHybridRendererContext = createContext<SduiHybridRenderers>({});