@proulr/sdui-components 0.1.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.
- package/README.md +29 -0
- package/package.json +60 -0
- package/src/bdc/compile-bdc-to-wire.ts +123 -0
- package/src/bdc/components/discover.tsx +55 -0
- package/src/bdc/components/forms.tsx +20 -0
- package/src/bdc/components/native-blocks.tsx +35 -0
- package/src/bdc/components/social.tsx +27 -0
- package/src/bdc/components/ui.tsx +114 -0
- package/src/bdc/index.ts +41 -0
- package/src/bdc/jsx-runtime.ts +49 -0
- package/src/catalog/block-types.ts +18 -0
- package/src/catalog/style-tokens.ts +48 -0
- package/src/global.d.ts +1 -0
- package/src/renderer/SduiScreenRenderer.tsx +28 -0
- package/src/renderer/ads/native-ad-catalog.ts +23 -0
- package/src/renderer/blocks/SduiDiscoverBlocks.tsx +470 -0
- package/src/renderer/blocks/SduiLayoutBlocks.tsx +177 -0
- package/src/renderer/blocks/SduiNativeAdBlock.tsx +201 -0
- package/src/renderer/blocks/SduiNativeAdBlock.web.tsx +6 -0
- package/src/renderer/blocks/SduiNativeDomainBlocks.tsx +141 -0
- package/src/renderer/blocks/SduiPrimitiveBlocks.tsx +158 -0
- package/src/renderer/blocks/SduiSocialBlocks.tsx +64 -0
- package/src/renderer/index.ts +45 -0
- package/src/renderer/lib/brazilian-date-input.ts +22 -0
- package/src/renderer/lib/discover-profile-formatters.ts +75 -0
- package/src/renderer/lib/resolve-data-ref.ts +21 -0
- package/src/renderer/sdui-chrome-scroll-context.tsx +40 -0
- package/src/renderer/sdui-component-registry.tsx +129 -0
- package/src/renderer/sdui-hybrid-renderer-context.tsx +91 -0
- package/src/renderer/sdui-layout-debug.tsx +107 -0
- package/src/renderer/theme/proulr-theme.ts +164 -0
- package/src/renderer/theme/resolve-style-props.ts +85 -0
- package/src/renderer/ui/AppButton.tsx +71 -0
- package/src/renderer/ui/AppLoadingSpinner.tsx +38 -0
- package/src/renderer/ui/AuthStepShell.tsx +128 -0
- package/src/renderer/ui/Card.tsx +13 -0
- package/src/renderer/ui/DateField.tsx +36 -0
- package/src/renderer/ui/InfoBanner.tsx +51 -0
- package/src/renderer/ui/TextField.tsx +81 -0
- package/src/renderer/ui/compose-audience-pill.tsx +47 -0
- package/src/renderer/ui/compose-glyphs.tsx +400 -0
- package/src/renderer/ui/compose-media-carousel.tsx +116 -0
- package/src/renderer/ui/compose-media-shortcut.tsx +80 -0
- package/src/renderer/ui/compose-recent-media-grid.tsx +268 -0
- package/src/renderer/ui/compose-reply-audience-sheet.tsx +155 -0
- package/src/renderer/ui/compose-screen-header.tsx +115 -0
- package/src/renderer/ui/compose-text-area.tsx +94 -0
- package/src/renderer/ui/compose-toolbar-icon.tsx +45 -0
- package/src/renderer/ui/compose-toolbar.tsx +57 -0
- package/src/renderer/ui/compose-visibility-row.tsx +51 -0
- package/src/renderer/ui/floating-create-menu.tsx +165 -0
- package/src/renderer/ui/index.ts +44 -0
- package/src/renderer/ui/story-compose-header.tsx +72 -0
- package/src/renderer/ui/story-create-shortcut-row.tsx +89 -0
- package/src/renderer/ui/story-editor-chrome.tsx +231 -0
- package/src/renderer/ui/story-glyphs.tsx +144 -0
- package/src/renderer/ui/story-surface-picker.tsx +124 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type DiscoverNearbyProfileWire = {
|
|
2
|
+
id: string;
|
|
3
|
+
display_name: string | null;
|
|
4
|
+
birth_date: string | null;
|
|
5
|
+
distance_m: number;
|
|
6
|
+
primary_photo_path: string | null;
|
|
7
|
+
photo_url?: string | null;
|
|
8
|
+
is_online: boolean | null;
|
|
9
|
+
last_seen?: string | null;
|
|
10
|
+
is_creator?: boolean | null;
|
|
11
|
+
has_new_media?: boolean | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type DiscoverGridProfileItemWire = {
|
|
15
|
+
kind: 'profile';
|
|
16
|
+
id: string;
|
|
17
|
+
person: DiscoverNearbyProfileWire;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type DiscoverGridAdItemWire = {
|
|
21
|
+
kind: 'ad';
|
|
22
|
+
id: string;
|
|
23
|
+
placement?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type DiscoverGridItemWire = DiscoverGridProfileItemWire | DiscoverGridAdItemWire;
|
|
27
|
+
|
|
28
|
+
export function profileAgeFromBirthDate(birthDate: string | null | undefined): number | null {
|
|
29
|
+
if (!birthDate) return null;
|
|
30
|
+
const born = new Date(birthDate);
|
|
31
|
+
if (Number.isNaN(born.getTime())) return null;
|
|
32
|
+
const today = new Date();
|
|
33
|
+
let age = today.getFullYear() - born.getFullYear();
|
|
34
|
+
const monthDelta = today.getMonth() - born.getMonth();
|
|
35
|
+
if (monthDelta < 0 || (monthDelta === 0 && today.getDate() < born.getDate())) {
|
|
36
|
+
age -= 1;
|
|
37
|
+
}
|
|
38
|
+
return age >= 0 ? age : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatDistanceLabel(distanceM: number): string {
|
|
42
|
+
if (distanceM < 1000) return `${Math.round(distanceM)} m`;
|
|
43
|
+
const km = distanceM / 1000;
|
|
44
|
+
return km < 10 ? `${km.toFixed(1)} km` : `${Math.round(km)} km`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type PresenceStatus = 'online' | 'recent' | 'offline';
|
|
48
|
+
|
|
49
|
+
export function getPresenceStatus(lastSeen?: string | null): PresenceStatus {
|
|
50
|
+
if (!lastSeen) return 'offline';
|
|
51
|
+
const seenAt = new Date(lastSeen).getTime();
|
|
52
|
+
if (Number.isNaN(seenAt)) return 'offline';
|
|
53
|
+
const deltaMs = Date.now() - seenAt;
|
|
54
|
+
if (deltaMs <= 5 * 60 * 1000) return 'online';
|
|
55
|
+
if (deltaMs <= 24 * 60 * 60 * 1000) return 'recent';
|
|
56
|
+
return 'offline';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isDiscoverGridItem(value: unknown): value is DiscoverGridItemWire {
|
|
60
|
+
if (!value || typeof value !== 'object') return false;
|
|
61
|
+
const kind = (value as { kind?: unknown }).kind;
|
|
62
|
+
return kind === 'profile' || kind === 'ad';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readDiscoverGridItems(data: Record<string, unknown>, ref: string): DiscoverGridItemWire[] {
|
|
66
|
+
const raw = ref.split('.').reduce<unknown>((acc, key) => {
|
|
67
|
+
if (acc && typeof acc === 'object' && key in (acc as Record<string, unknown>)) {
|
|
68
|
+
return (acc as Record<string, unknown>)[key];
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}, data);
|
|
72
|
+
|
|
73
|
+
if (!Array.isArray(raw)) return [];
|
|
74
|
+
return raw.filter(isDiscoverGridItem);
|
|
75
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function resolveDataRef(data: Record<string, unknown>, ref?: unknown): unknown {
|
|
2
|
+
if (typeof ref !== 'string' || !ref) return undefined;
|
|
3
|
+
|
|
4
|
+
return ref.split('.').reduce<unknown>((acc, key) => {
|
|
5
|
+
if (acc && typeof acc === 'object' && key in (acc as Record<string, unknown>)) {
|
|
6
|
+
return (acc as Record<string, unknown>)[key];
|
|
7
|
+
}
|
|
8
|
+
return undefined;
|
|
9
|
+
}, data);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveDataRefString(data: Record<string, unknown>, ref?: unknown): string {
|
|
13
|
+
const value = resolveDataRef(data, ref);
|
|
14
|
+
if (value == null) return '';
|
|
15
|
+
return String(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveDataRefBoolean(data: Record<string, unknown>, ref?: unknown): boolean {
|
|
19
|
+
const value = resolveDataRef(data, ref);
|
|
20
|
+
return value === true || value === 'true' || value === 1 || value === '1';
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { createContext, useContext, type ReactNode } from 'react';
|
|
2
|
+
import type { ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type SduiChromeScrollInsetProps = {
|
|
5
|
+
contentInset?: { top: number; bottom: number };
|
|
6
|
+
contentOffset?: { x: number; y: number };
|
|
7
|
+
automaticallyAdjustContentInsets?: boolean;
|
|
8
|
+
scrollIndicatorInsets?: { top: number; bottom: number };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SduiChromeScrollInsets = {
|
|
12
|
+
top: number;
|
|
13
|
+
bottom: number;
|
|
14
|
+
headerHeight: number;
|
|
15
|
+
tabBarHeight: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type SduiChromeScrollConfig = {
|
|
19
|
+
scrollInsetProps: SduiChromeScrollInsetProps;
|
|
20
|
+
contentContainerStyle: ViewStyle;
|
|
21
|
+
chromeInsets: SduiChromeScrollInsets;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const SduiChromeScrollContext = createContext<SduiChromeScrollConfig | null>(null);
|
|
25
|
+
|
|
26
|
+
export function SduiChromeScrollProvider({
|
|
27
|
+
value,
|
|
28
|
+
children,
|
|
29
|
+
}: {
|
|
30
|
+
value: SduiChromeScrollConfig;
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
}) {
|
|
33
|
+
return (
|
|
34
|
+
<SduiChromeScrollContext.Provider value={value}>{children}</SduiChromeScrollContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useSduiChromeScroll(): SduiChromeScrollConfig | null {
|
|
39
|
+
return useContext(SduiChromeScrollContext);
|
|
40
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
+
import type { SduiBlock } from '@proulr/sdui-contract';
|
|
3
|
+
import {
|
|
4
|
+
SduiButtonBlock,
|
|
5
|
+
SduiColumnBlock,
|
|
6
|
+
SduiInfoBannerBlock,
|
|
7
|
+
SduiRowBlock,
|
|
8
|
+
SduiScrollBlock,
|
|
9
|
+
SduiSpacerBlock,
|
|
10
|
+
SduiTextBlock,
|
|
11
|
+
SduiUnknownBlock,
|
|
12
|
+
type SduiBlockRenderProps,
|
|
13
|
+
} from './blocks/SduiPrimitiveBlocks';
|
|
14
|
+
import {
|
|
15
|
+
SduiCardBlock,
|
|
16
|
+
SduiListItemBlock,
|
|
17
|
+
SduiSectionHeaderBlock,
|
|
18
|
+
SduiToggleRowBlock,
|
|
19
|
+
} from './blocks/SduiLayoutBlocks';
|
|
20
|
+
import {
|
|
21
|
+
SduiChipBlock,
|
|
22
|
+
SduiChipRowBlock,
|
|
23
|
+
SduiDiscoverGridBlock,
|
|
24
|
+
SduiDiscoverMapBlock,
|
|
25
|
+
SduiDiscoverProfileCardBlock,
|
|
26
|
+
SduiTextFieldBlock,
|
|
27
|
+
} from './blocks/SduiDiscoverBlocks';
|
|
28
|
+
import {
|
|
29
|
+
SduiProfileCompletionBannerBlock,
|
|
30
|
+
SduiStoryRailBlock,
|
|
31
|
+
} from './blocks/SduiSocialBlocks';
|
|
32
|
+
import { SduiNativeAdBlock } from './blocks/SduiNativeAdBlock';
|
|
33
|
+
import {
|
|
34
|
+
SduiAlbumDetailBlock,
|
|
35
|
+
SduiAlbumGridBlock,
|
|
36
|
+
SduiChatInboxListBlock,
|
|
37
|
+
SduiChatThreadBlock,
|
|
38
|
+
SduiFlashInboxListBlock,
|
|
39
|
+
SduiNewPostComposerBlock,
|
|
40
|
+
SduiPostCommentsBlock,
|
|
41
|
+
SduiProfileOnboardingBlock,
|
|
42
|
+
SduiProfileSelfBlock,
|
|
43
|
+
SduiProfileSetupBlock,
|
|
44
|
+
SduiProfileViewBlock,
|
|
45
|
+
SduiSocialPostFeedBlock,
|
|
46
|
+
SduiSocialProfileBlock,
|
|
47
|
+
SduiSettingsHomeBlock,
|
|
48
|
+
SduiDiscoverBrowseLocationBlock,
|
|
49
|
+
SduiThoughtFeedBlock,
|
|
50
|
+
SduiVerticalVideoPagerBlock,
|
|
51
|
+
} from './blocks/SduiNativeDomainBlocks';
|
|
52
|
+
|
|
53
|
+
export type { SduiBlockRenderProps };
|
|
54
|
+
|
|
55
|
+
type SduiBlockComponent = ComponentType<SduiBlockRenderProps>;
|
|
56
|
+
|
|
57
|
+
const REGISTRY: Record<string, SduiBlockComponent> = {
|
|
58
|
+
text: SduiTextBlock,
|
|
59
|
+
column: SduiColumnBlock,
|
|
60
|
+
row: SduiRowBlock,
|
|
61
|
+
scroll: SduiScrollBlock,
|
|
62
|
+
button: SduiButtonBlock,
|
|
63
|
+
spacer: SduiSpacerBlock,
|
|
64
|
+
info_banner: SduiInfoBannerBlock,
|
|
65
|
+
native_ad: SduiNativeAdBlock,
|
|
66
|
+
card: SduiCardBlock,
|
|
67
|
+
section_header: SduiSectionHeaderBlock,
|
|
68
|
+
list_item: SduiListItemBlock,
|
|
69
|
+
toggle_row: SduiToggleRowBlock,
|
|
70
|
+
chip: SduiChipBlock,
|
|
71
|
+
chip_row: SduiChipRowBlock,
|
|
72
|
+
discover_grid: SduiDiscoverGridBlock,
|
|
73
|
+
discover_profile_card: SduiDiscoverProfileCardBlock,
|
|
74
|
+
discover_map: SduiDiscoverMapBlock,
|
|
75
|
+
text_field: SduiTextFieldBlock,
|
|
76
|
+
story_rail: SduiStoryRailBlock,
|
|
77
|
+
profile_completion_banner: SduiProfileCompletionBannerBlock,
|
|
78
|
+
flash_inbox_list: SduiFlashInboxListBlock,
|
|
79
|
+
chat_inbox_list: SduiChatInboxListBlock,
|
|
80
|
+
chat_thread: SduiChatThreadBlock,
|
|
81
|
+
social_post_feed: SduiSocialPostFeedBlock,
|
|
82
|
+
thought_feed: SduiThoughtFeedBlock,
|
|
83
|
+
vertical_video_pager: SduiVerticalVideoPagerBlock,
|
|
84
|
+
profile_view: SduiProfileViewBlock,
|
|
85
|
+
profile_self: SduiProfileSelfBlock,
|
|
86
|
+
profile_setup: SduiProfileSetupBlock,
|
|
87
|
+
profile_onboarding: SduiProfileOnboardingBlock,
|
|
88
|
+
album_grid: SduiAlbumGridBlock,
|
|
89
|
+
album_detail: SduiAlbumDetailBlock,
|
|
90
|
+
new_post_composer: SduiNewPostComposerBlock,
|
|
91
|
+
post_comments: SduiPostCommentsBlock,
|
|
92
|
+
social_profile: SduiSocialProfileBlock,
|
|
93
|
+
settings_home: SduiSettingsHomeBlock,
|
|
94
|
+
discover_browse_location: SduiDiscoverBrowseLocationBlock,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export function resolveSduiBlockComponent(type: string): SduiBlockComponent {
|
|
98
|
+
return REGISTRY[type] ?? SduiUnknownBlock;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function registerSduiBlockComponent(type: string, component: SduiBlockComponent) {
|
|
102
|
+
REGISTRY[type] = component;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type BlockNodeProps = {
|
|
106
|
+
block: SduiBlock;
|
|
107
|
+
data: Record<string, unknown>;
|
|
108
|
+
onAction: (actionId: string, vars?: Record<string, string>) => void;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export function renderBlockTree(
|
|
112
|
+
block: SduiBlock,
|
|
113
|
+
data: Record<string, unknown>,
|
|
114
|
+
onAction: BlockNodeProps['onAction'],
|
|
115
|
+
): ReactNode {
|
|
116
|
+
const Component = resolveSduiBlockComponent(block.type);
|
|
117
|
+
const childNodes = block.children.map((child) => (
|
|
118
|
+
<BlockNode key={child.id} block={child} data={data} onAction={onAction} />
|
|
119
|
+
));
|
|
120
|
+
return (
|
|
121
|
+
<Component block={block} data={data} onAction={onAction}>
|
|
122
|
+
{childNodes}
|
|
123
|
+
</Component>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function BlockNode({ block, data, onAction }: BlockNodeProps) {
|
|
128
|
+
return <>{renderBlockTree(block, data, onAction)}</>;
|
|
129
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { createContext, useContext, type ReactNode } from 'react';
|
|
2
|
+
import type { SduiBlock } from '@proulr/sdui-contract';
|
|
3
|
+
import type { DiscoverNearbyProfileWire } from './lib/discover-profile-formatters';
|
|
4
|
+
|
|
5
|
+
export type SduiHybridStoryRailProps = {
|
|
6
|
+
surface: 'discover' | 'vibes';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type SduiHybridProfileCompletionBannerProps = {
|
|
10
|
+
percent: number;
|
|
11
|
+
onDismiss: () => void;
|
|
12
|
+
onComplete: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SduiHybridDiscoverMapProps = {
|
|
16
|
+
center: { lat: number; lng: number };
|
|
17
|
+
label: string;
|
|
18
|
+
height: number;
|
|
19
|
+
onCenterChange: (coords: { lat: number; lng: number }) => void;
|
|
20
|
+
centerMoveToken: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type SduiHybridDiscoverProfileCardProps = {
|
|
24
|
+
person: DiscoverNearbyProfileWire;
|
|
25
|
+
width: number;
|
|
26
|
+
onPress: () => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type SduiNativeBlockRenderProps = {
|
|
30
|
+
block: SduiBlock;
|
|
31
|
+
data: Record<string, unknown>;
|
|
32
|
+
onAction: (actionId: string, vars?: Record<string, string>) => void;
|
|
33
|
+
dataRef?: string;
|
|
34
|
+
conversationIdRef?: string;
|
|
35
|
+
peerIdRef?: string;
|
|
36
|
+
peerNameRef?: string;
|
|
37
|
+
handleRef?: string;
|
|
38
|
+
postIdRef?: string;
|
|
39
|
+
albumIdRef?: string;
|
|
40
|
+
profileIdRef?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type SduiHybridRenderers = {
|
|
44
|
+
renderStoryRail?: (props: SduiHybridStoryRailProps) => ReactNode;
|
|
45
|
+
renderProfileCompletionBanner?: (props: SduiHybridProfileCompletionBannerProps) => ReactNode;
|
|
46
|
+
renderDiscoverMap?: (props: SduiHybridDiscoverMapProps) => ReactNode;
|
|
47
|
+
renderDiscoverProfileCard?: (props: SduiHybridDiscoverProfileCardProps) => ReactNode;
|
|
48
|
+
renderTextField?: (props: {
|
|
49
|
+
dataRef: string;
|
|
50
|
+
placeholder: string;
|
|
51
|
+
value: string;
|
|
52
|
+
submitActionId?: string;
|
|
53
|
+
onSubmit: () => void;
|
|
54
|
+
}) => ReactNode;
|
|
55
|
+
onDiscoverMapCenterChange?: (coords: { lat: number; lng: number }) => void;
|
|
56
|
+
renderFlashInboxList?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
57
|
+
renderChatInboxList?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
58
|
+
renderChatThread?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
59
|
+
renderSocialPostFeed?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
60
|
+
renderThoughtFeed?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
61
|
+
renderVerticalVideoPager?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
62
|
+
renderProfileView?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
63
|
+
renderProfileSelf?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
64
|
+
renderProfileSetup?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
65
|
+
renderProfileOnboarding?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
66
|
+
renderAlbumGrid?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
67
|
+
renderAlbumDetail?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
68
|
+
renderNewPostComposer?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
69
|
+
renderPostComments?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
70
|
+
renderSocialProfile?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
71
|
+
renderSettingsHome?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
72
|
+
renderDiscoverBrowseLocation?: (props: SduiNativeBlockRenderProps) => ReactNode;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const SduiHybridRendererContext = createContext<SduiHybridRenderers>({});
|
|
76
|
+
|
|
77
|
+
export function SduiHybridRendererProvider({
|
|
78
|
+
value,
|
|
79
|
+
children,
|
|
80
|
+
}: {
|
|
81
|
+
value: SduiHybridRenderers;
|
|
82
|
+
children: ReactNode;
|
|
83
|
+
}) {
|
|
84
|
+
return (
|
|
85
|
+
<SduiHybridRendererContext.Provider value={value}>{children}</SduiHybridRendererContext.Provider>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function useSduiHybridRenderer(): SduiHybridRenderers {
|
|
90
|
+
return useContext(SduiHybridRendererContext);
|
|
91
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
|
+
import { StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/** Desligado por padrão. Ative com EXPO_PUBLIC_SDUI_LAYOUT_DEBUG=true no .env */
|
|
5
|
+
export const SDUI_LAYOUT_DEBUG =
|
|
6
|
+
typeof __DEV__ !== 'undefined' &&
|
|
7
|
+
__DEV__ &&
|
|
8
|
+
process.env.EXPO_PUBLIC_SDUI_LAYOUT_DEBUG === 'true';
|
|
9
|
+
|
|
10
|
+
export type SduiLayoutDebugLayer = 'renderer' | 'block' | 'column' | 'scroll' | 'native';
|
|
11
|
+
|
|
12
|
+
const LAYER_STYLES: Record<
|
|
13
|
+
SduiLayoutDebugLayer,
|
|
14
|
+
{ borderColor: string; backgroundColor: string; label: string }
|
|
15
|
+
> = {
|
|
16
|
+
renderer: {
|
|
17
|
+
borderColor: '#34C759',
|
|
18
|
+
backgroundColor: 'rgba(52,199,89,0.10)',
|
|
19
|
+
label: 'SduiScreenRenderer',
|
|
20
|
+
},
|
|
21
|
+
block: {
|
|
22
|
+
borderColor: '#007AFF',
|
|
23
|
+
backgroundColor: 'rgba(0,122,255,0.08)',
|
|
24
|
+
label: 'block root',
|
|
25
|
+
},
|
|
26
|
+
column: {
|
|
27
|
+
borderColor: '#5856D6',
|
|
28
|
+
backgroundColor: 'rgba(88,86,214,0.08)',
|
|
29
|
+
label: 'Column',
|
|
30
|
+
},
|
|
31
|
+
scroll: {
|
|
32
|
+
borderColor: '#32ADE6',
|
|
33
|
+
backgroundColor: 'rgba(50,173,230,0.08)',
|
|
34
|
+
label: 'Scroll',
|
|
35
|
+
},
|
|
36
|
+
native: {
|
|
37
|
+
borderColor: '#AF52DE',
|
|
38
|
+
backgroundColor: 'rgba(175,82,222,0.10)',
|
|
39
|
+
label: 'NativeBlockShell',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function sduiDebugViewStyle(layer: SduiLayoutDebugLayer): ViewStyle {
|
|
44
|
+
if (!SDUI_LAYOUT_DEBUG) return {};
|
|
45
|
+
const layerStyle = LAYER_STYLES[layer];
|
|
46
|
+
return {
|
|
47
|
+
borderWidth: 2,
|
|
48
|
+
borderColor: layerStyle.borderColor,
|
|
49
|
+
backgroundColor: layerStyle.backgroundColor,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function SduiLayoutDebugLabel({
|
|
54
|
+
layer,
|
|
55
|
+
extra,
|
|
56
|
+
}: {
|
|
57
|
+
layer: SduiLayoutDebugLayer;
|
|
58
|
+
extra?: string;
|
|
59
|
+
}) {
|
|
60
|
+
if (!SDUI_LAYOUT_DEBUG) return null;
|
|
61
|
+
const layerStyle = LAYER_STYLES[layer];
|
|
62
|
+
return (
|
|
63
|
+
<View style={styles.labelHost} pointerEvents="none">
|
|
64
|
+
<Text style={styles.labelText}>
|
|
65
|
+
{layerStyle.label}
|
|
66
|
+
{extra ? ` · ${extra}` : ''}
|
|
67
|
+
</Text>
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function SduiLayoutDebugFrame({
|
|
73
|
+
layer,
|
|
74
|
+
extra,
|
|
75
|
+
style,
|
|
76
|
+
children,
|
|
77
|
+
}: {
|
|
78
|
+
layer: SduiLayoutDebugLayer;
|
|
79
|
+
extra?: string;
|
|
80
|
+
style?: StyleProp<ViewStyle>;
|
|
81
|
+
children: ReactNode;
|
|
82
|
+
}) {
|
|
83
|
+
return (
|
|
84
|
+
<View style={[sduiDebugViewStyle(layer), style]}>
|
|
85
|
+
<SduiLayoutDebugLabel layer={layer} extra={extra} />
|
|
86
|
+
{children}
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const styles = StyleSheet.create({
|
|
92
|
+
labelHost: {
|
|
93
|
+
position: 'absolute',
|
|
94
|
+
top: 2,
|
|
95
|
+
left: 2,
|
|
96
|
+
zIndex: 9999,
|
|
97
|
+
backgroundColor: 'rgba(0,0,0,0.82)',
|
|
98
|
+
paddingHorizontal: 6,
|
|
99
|
+
paddingVertical: 2,
|
|
100
|
+
borderRadius: 4,
|
|
101
|
+
},
|
|
102
|
+
labelText: {
|
|
103
|
+
color: '#FFFFFF',
|
|
104
|
+
fontSize: 10,
|
|
105
|
+
fontWeight: '700',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Platform, ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/** Proulr UI theme — Night Stalker OLED. */
|
|
4
|
+
|
|
5
|
+
export const m3Colors = {
|
|
6
|
+
primary: '#D946EF',
|
|
7
|
+
onPrimary: '#FFFFFF',
|
|
8
|
+
primaryContainer: 'rgba(217,70,239,0.22)',
|
|
9
|
+
onPrimaryContainer: '#F0ABFC',
|
|
10
|
+
secondary: '#00E5FF',
|
|
11
|
+
onSecondary: '#000000',
|
|
12
|
+
secondaryContainer: 'rgba(0,229,255,0.18)',
|
|
13
|
+
onSecondaryContainer: '#84FFFF',
|
|
14
|
+
tertiary: '#FF2D55',
|
|
15
|
+
onTertiary: '#FFFFFF',
|
|
16
|
+
tertiaryContainer: 'rgba(255,45,85,0.20)',
|
|
17
|
+
onTertiaryContainer: '#FF8FA8',
|
|
18
|
+
surface: '#000000',
|
|
19
|
+
onSurface: '#FFFFFF',
|
|
20
|
+
onSurfaceVariant: '#A3A3A3',
|
|
21
|
+
surfaceContainer: '#0A0A0A',
|
|
22
|
+
surfaceContainerHigh: '#141414',
|
|
23
|
+
surfaceContainerHighest: '#222222',
|
|
24
|
+
outline: '#1A1A1A',
|
|
25
|
+
outlineVariant: '#2A2A2A',
|
|
26
|
+
error: '#FF4444',
|
|
27
|
+
onError: '#FFFFFF',
|
|
28
|
+
errorContainer: 'rgba(255,68,68,0.22)',
|
|
29
|
+
onErrorContainer: '#FF8A80',
|
|
30
|
+
inverseSurface: '#FFFFFF',
|
|
31
|
+
inverseOnSurface: '#000000',
|
|
32
|
+
scrim: 'rgba(0,0,0,0.78)',
|
|
33
|
+
online: '#34FF88',
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export const m3Shape = {
|
|
37
|
+
cornerExtraSmall: 4,
|
|
38
|
+
cornerSmall: 8,
|
|
39
|
+
cornerMedium: 12,
|
|
40
|
+
cornerLarge: 16,
|
|
41
|
+
cornerExtraLarge: 24,
|
|
42
|
+
cornerFull: 999,
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
export const m3Type = {
|
|
46
|
+
display: { fontFamily: 'Montserrat_800ExtraBold', fontSize: 57, lineHeight: 64 },
|
|
47
|
+
headline: { fontFamily: 'Montserrat_700Bold', fontSize: 28, lineHeight: 36 },
|
|
48
|
+
title: { fontFamily: 'Montserrat_600SemiBold', fontSize: 22, lineHeight: 28 },
|
|
49
|
+
titleSmall: { fontFamily: 'Montserrat_600SemiBold', fontSize: 16, lineHeight: 24 },
|
|
50
|
+
body: { fontFamily: 'Montserrat_400Regular', fontSize: 16, lineHeight: 24 },
|
|
51
|
+
bodySmall: { fontFamily: 'Montserrat_400Regular', fontSize: 14, lineHeight: 20 },
|
|
52
|
+
label: { fontFamily: 'Montserrat_500Medium', fontSize: 14, lineHeight: 20 },
|
|
53
|
+
labelSmall: { fontFamily: 'Montserrat_500Medium', fontSize: 12, lineHeight: 16 },
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
export const colors = {
|
|
57
|
+
bg: '#000000',
|
|
58
|
+
surface: '#0A0A0A',
|
|
59
|
+
surface2: '#141414',
|
|
60
|
+
border: '#1A1A1A',
|
|
61
|
+
borderStrong: '#2A2A2A',
|
|
62
|
+
textPrimary: '#FFFFFF',
|
|
63
|
+
textSecondary: '#A3A3A3',
|
|
64
|
+
textMuted: '#666666',
|
|
65
|
+
white: '#FFFFFF',
|
|
66
|
+
textOnAccent: '#FFFFFF',
|
|
67
|
+
accent: '#D946EF',
|
|
68
|
+
accentSoft: 'rgba(217,70,239,0.22)',
|
|
69
|
+
accentText: '#F0ABFC',
|
|
70
|
+
highlight: '#00E5FF',
|
|
71
|
+
highlightSoft: 'rgba(0,229,255,0.18)',
|
|
72
|
+
headerStart: '#2D0A35',
|
|
73
|
+
headerEnd: '#D946EF',
|
|
74
|
+
online: '#34FF88',
|
|
75
|
+
onlineSoft: 'rgba(52,255,136,0.18)',
|
|
76
|
+
danger: '#FF4444',
|
|
77
|
+
dangerSoft: 'rgba(255,68,68,0.22)',
|
|
78
|
+
overlay: 'rgba(0,0,0,0.78)',
|
|
79
|
+
surfaceGlass: 'rgba(255,255,255,0.06)',
|
|
80
|
+
primary: m3Colors.primary,
|
|
81
|
+
onSurface: m3Colors.onSurface,
|
|
82
|
+
onSurfaceVariant: m3Colors.onSurfaceVariant,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const radius = {
|
|
86
|
+
sm: 8,
|
|
87
|
+
md: 12,
|
|
88
|
+
lg: 16,
|
|
89
|
+
xl: 24,
|
|
90
|
+
pill: 999,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const spacing = {
|
|
94
|
+
xs: 4,
|
|
95
|
+
sm: 8,
|
|
96
|
+
md: 12,
|
|
97
|
+
lg: 16,
|
|
98
|
+
xl: 24,
|
|
99
|
+
xxl: 32,
|
|
100
|
+
} as const;
|
|
101
|
+
|
|
102
|
+
export const gradients = {
|
|
103
|
+
brand: ['#7A0D8A', '#D946EF'] as const,
|
|
104
|
+
header: ['#2D0A35', '#D946EF'] as const,
|
|
105
|
+
headerGlow: ['rgba(217,70,239,0.20)', 'rgba(217,70,239,0)'] as const,
|
|
106
|
+
neonBorder: [colors.headerEnd, colors.accent] as const,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const fonts = {
|
|
110
|
+
regular: 'Montserrat_400Regular',
|
|
111
|
+
medium: 'Montserrat_500Medium',
|
|
112
|
+
semibold: 'Montserrat_600SemiBold',
|
|
113
|
+
bold: 'Montserrat_700Bold',
|
|
114
|
+
extrabold: 'Montserrat_800ExtraBold',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const shadows = {
|
|
118
|
+
card: Platform.select<ViewStyle>({
|
|
119
|
+
ios: {
|
|
120
|
+
shadowColor: '#D946EF',
|
|
121
|
+
shadowOffset: { width: 0, height: 4 },
|
|
122
|
+
shadowOpacity: 0.12,
|
|
123
|
+
shadowRadius: 12,
|
|
124
|
+
},
|
|
125
|
+
android: { elevation: 3 },
|
|
126
|
+
default: {},
|
|
127
|
+
}),
|
|
128
|
+
elevated: Platform.select<ViewStyle>({
|
|
129
|
+
ios: {
|
|
130
|
+
shadowColor: '#00E5FF',
|
|
131
|
+
shadowOffset: { width: 0, height: 6 },
|
|
132
|
+
shadowOpacity: 0.14,
|
|
133
|
+
shadowRadius: 16,
|
|
134
|
+
},
|
|
135
|
+
android: { elevation: 6 },
|
|
136
|
+
default: {},
|
|
137
|
+
}),
|
|
138
|
+
subtle: Platform.select<ViewStyle>({
|
|
139
|
+
ios: {
|
|
140
|
+
shadowColor: '#000000',
|
|
141
|
+
shadowOffset: { width: 0, height: 2 },
|
|
142
|
+
shadowOpacity: 0.4,
|
|
143
|
+
shadowRadius: 6,
|
|
144
|
+
},
|
|
145
|
+
android: { elevation: 2 },
|
|
146
|
+
default: {},
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const m3Elevation = {
|
|
151
|
+
level0: {},
|
|
152
|
+
level1: shadows.subtle,
|
|
153
|
+
level2: shadows.card,
|
|
154
|
+
level3: shadows.elevated,
|
|
155
|
+
} as const;
|
|
156
|
+
|
|
157
|
+
export const cardStyle = {
|
|
158
|
+
backgroundColor: m3Colors.surfaceContainer,
|
|
159
|
+
borderRadius: m3Shape.cornerLarge,
|
|
160
|
+
padding: spacing.sm,
|
|
161
|
+
borderWidth: 1,
|
|
162
|
+
borderColor: m3Colors.outlineVariant,
|
|
163
|
+
...m3Elevation.level2,
|
|
164
|
+
} as const;
|