@oxyhq/bloom 0.22.0 → 0.24.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/lib/commonjs/content-panel/index.js +12 -6
- package/lib/commonjs/content-panel/index.js.map +1 -1
- package/lib/commonjs/content-panel/index.web.js +31 -21
- package/lib/commonjs/content-panel/index.web.js.map +1 -1
- package/lib/commonjs/content-panel/nesting-context.js +39 -0
- package/lib/commonjs/content-panel/nesting-context.js.map +1 -0
- package/lib/commonjs/index.js +20 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +20 -12
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/link-preview/LinkPreviewCard.js +133 -0
- package/lib/commonjs/link-preview/LinkPreviewCard.js.map +1 -0
- package/lib/commonjs/link-preview/index.js +13 -0
- package/lib/commonjs/link-preview/index.js.map +1 -0
- package/lib/commonjs/link-preview/types.js +6 -0
- package/lib/commonjs/link-preview/types.js.map +1 -0
- package/lib/module/content-panel/index.js +12 -6
- package/lib/module/content-panel/index.js.map +1 -1
- package/lib/module/content-panel/index.web.js +31 -21
- package/lib/module/content-panel/index.web.js.map +1 -1
- package/lib/module/content-panel/nesting-context.js +35 -0
- package/lib/module/content-panel/nesting-context.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/link-preview/LinkPreviewCard.js +127 -0
- package/lib/module/link-preview/LinkPreviewCard.js.map +1 -0
- package/lib/module/link-preview/index.js +4 -0
- package/lib/module/link-preview/index.js.map +1 -0
- package/lib/module/link-preview/types.js +4 -0
- package/lib/module/link-preview/types.js.map +1 -0
- package/lib/typescript/commonjs/content-panel/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/content-panel/nesting-context.d.ts +24 -0
- package/lib/typescript/commonjs/content-panel/nesting-context.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/link-preview/LinkPreviewCard.d.ts +24 -0
- package/lib/typescript/commonjs/link-preview/LinkPreviewCard.d.ts.map +1 -0
- package/lib/typescript/commonjs/link-preview/index.d.ts +3 -0
- package/lib/typescript/commonjs/link-preview/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/link-preview/types.d.ts +32 -0
- package/lib/typescript/commonjs/link-preview/types.d.ts.map +1 -0
- package/lib/typescript/module/content-panel/index.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/index.web.d.ts.map +1 -1
- package/lib/typescript/module/content-panel/nesting-context.d.ts +24 -0
- package/lib/typescript/module/content-panel/nesting-context.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/link-preview/LinkPreviewCard.d.ts +24 -0
- package/lib/typescript/module/link-preview/LinkPreviewCard.d.ts.map +1 -0
- package/lib/typescript/module/link-preview/index.d.ts +3 -0
- package/lib/typescript/module/link-preview/index.d.ts.map +1 -0
- package/lib/typescript/module/link-preview/types.d.ts +32 -0
- package/lib/typescript/module/link-preview/types.d.ts.map +1 -0
- package/package.json +18 -1
- package/src/__tests__/ContentPanelNesting.test.tsx +52 -0
- package/src/__tests__/LinkPreviewCard.test.tsx +60 -0
- package/src/content-panel/index.tsx +18 -8
- package/src/content-panel/index.web.tsx +40 -29
- package/src/content-panel/nesting-context.ts +35 -0
- package/src/index.ts +2 -0
- package/src/index.web.ts +2 -0
- package/src/link-preview/LinkPreviewCard.tsx +156 -0
- package/src/link-preview/index.ts +2 -0
- package/src/link-preview/types.ts +32 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LinkPreviewCard` — the canonical Oxy link-preview surface.
|
|
3
|
+
*
|
|
4
|
+
* One component renders the OpenGraph-style "cover image + site name + title +
|
|
5
|
+
* description" card across every Oxy app. It consumes the link-preview DATA
|
|
6
|
+
* produced by the Oxy SDK (`@oxyhq/core` `getLinkPreview` / `getLinkPreviews`,
|
|
7
|
+
* DTO `LinkPreview`) but takes a structural prop shape so Bloom never depends on
|
|
8
|
+
* `@oxyhq/contracts`.
|
|
9
|
+
*
|
|
10
|
+
* Styling is NativeWind-className-first (semantic Bloom utilities like
|
|
11
|
+
* `bg-card`, `border-border`, `text-foreground`, `text-muted-foreground`) with
|
|
12
|
+
* `useTheme()` Bloom tokens for the runtime-color and numeric-geometry values
|
|
13
|
+
* that have no clean utility — mirroring the `ContentPanel` idiom. The literal
|
|
14
|
+
* class strings must stay literal so a consumer's Tailwind content-scan over
|
|
15
|
+
* `lib/**` (and the native `src/**`) picks them up.
|
|
16
|
+
*
|
|
17
|
+
* The `image` prop is a plain absolute URL (e.g. a `cloud.oxy.so` OG image) and
|
|
18
|
+
* is rendered directly with the RN `Image` primitive — NOT through
|
|
19
|
+
* `ImageResolver` / `Avatar`, which resolve bare Oxy file IDs.
|
|
20
|
+
*/
|
|
21
|
+
import React, { memo, useCallback, useMemo } from 'react';
|
|
22
|
+
import {
|
|
23
|
+
View,
|
|
24
|
+
Text,
|
|
25
|
+
Image,
|
|
26
|
+
Pressable,
|
|
27
|
+
Linking,
|
|
28
|
+
} from 'react-native';
|
|
29
|
+
|
|
30
|
+
import { useTheme } from '../theme/use-theme';
|
|
31
|
+
import { useInteractionState } from '../hooks/useInteractionState';
|
|
32
|
+
import { RADIUS } from '../design-tokens/scales';
|
|
33
|
+
import type { LinkPreviewCardProps } from './types';
|
|
34
|
+
|
|
35
|
+
/** Height (px) of the optional cover image at the top of the card. */
|
|
36
|
+
const COVER_IMAGE_HEIGHT = 160;
|
|
37
|
+
|
|
38
|
+
/** Opacity applied to the whole card while it is pressed. */
|
|
39
|
+
const PRESSED_OPACITY = 0.85;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Best-effort hostname for the siteName / title fallbacks. Returns `undefined`
|
|
43
|
+
* for an unparseable URL so the caller can fall back further (to the raw URL).
|
|
44
|
+
*/
|
|
45
|
+
function hostnameOf(url: string): string | undefined {
|
|
46
|
+
try {
|
|
47
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const LinkPreviewCardComponent: React.FC<LinkPreviewCardProps> = ({
|
|
54
|
+
url,
|
|
55
|
+
title,
|
|
56
|
+
description,
|
|
57
|
+
image,
|
|
58
|
+
siteName,
|
|
59
|
+
onPress,
|
|
60
|
+
className,
|
|
61
|
+
style,
|
|
62
|
+
}) => {
|
|
63
|
+
const { colors } = useTheme();
|
|
64
|
+
const { state: pressed, onIn: onPressIn, onOut: onPressOut } =
|
|
65
|
+
useInteractionState();
|
|
66
|
+
|
|
67
|
+
const { displaySiteName, displayTitle } = useMemo(() => {
|
|
68
|
+
const host = hostnameOf(url);
|
|
69
|
+
return {
|
|
70
|
+
displaySiteName: siteName ?? host,
|
|
71
|
+
displayTitle: title ?? host ?? url,
|
|
72
|
+
};
|
|
73
|
+
}, [url, siteName, title]);
|
|
74
|
+
|
|
75
|
+
const handlePress = useCallback(() => {
|
|
76
|
+
if (onPress) {
|
|
77
|
+
onPress();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// No explicit handler: open the URL with the system handler. A rejected
|
|
81
|
+
// promise (unsupported scheme / no handler) is non-actionable for a
|
|
82
|
+
// presentational card, so it is swallowed rather than surfaced.
|
|
83
|
+
void Linking.openURL(url).catch(() => undefined);
|
|
84
|
+
}, [onPress, url]);
|
|
85
|
+
|
|
86
|
+
const surfaceClass = ['overflow-hidden border border-border bg-card', className]
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.join(' ');
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Pressable
|
|
92
|
+
{...({ className: surfaceClass } as Record<string, string>)}
|
|
93
|
+
style={[
|
|
94
|
+
{ borderRadius: RADIUS['radius-20'] },
|
|
95
|
+
pressed && { opacity: PRESSED_OPACITY },
|
|
96
|
+
style,
|
|
97
|
+
]}
|
|
98
|
+
onPress={handlePress}
|
|
99
|
+
onPressIn={onPressIn}
|
|
100
|
+
onPressOut={onPressOut}
|
|
101
|
+
accessibilityRole="link"
|
|
102
|
+
accessibilityLabel={displayTitle}
|
|
103
|
+
>
|
|
104
|
+
{image ? (
|
|
105
|
+
<Image
|
|
106
|
+
source={{ uri: image }}
|
|
107
|
+
resizeMode="cover"
|
|
108
|
+
{...({ className: 'w-full' } as Record<string, string>)}
|
|
109
|
+
style={{
|
|
110
|
+
height: COVER_IMAGE_HEIGHT,
|
|
111
|
+
backgroundColor: colors.backgroundTertiary,
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
) : null}
|
|
115
|
+
|
|
116
|
+
<View {...({ className: 'p-3' } as Record<string, string>)}>
|
|
117
|
+
{displaySiteName ? (
|
|
118
|
+
<Text
|
|
119
|
+
numberOfLines={1}
|
|
120
|
+
{...({
|
|
121
|
+
className:
|
|
122
|
+
'text-xs font-medium uppercase tracking-wide text-muted-foreground',
|
|
123
|
+
} as Record<string, string>)}
|
|
124
|
+
>
|
|
125
|
+
{displaySiteName}
|
|
126
|
+
</Text>
|
|
127
|
+
) : null}
|
|
128
|
+
|
|
129
|
+
{displayTitle ? (
|
|
130
|
+
<Text
|
|
131
|
+
numberOfLines={2}
|
|
132
|
+
{...({
|
|
133
|
+
className: 'text-[15px] font-semibold text-foreground mt-1',
|
|
134
|
+
} as Record<string, string>)}
|
|
135
|
+
>
|
|
136
|
+
{displayTitle}
|
|
137
|
+
</Text>
|
|
138
|
+
) : null}
|
|
139
|
+
|
|
140
|
+
{description ? (
|
|
141
|
+
<Text
|
|
142
|
+
numberOfLines={2}
|
|
143
|
+
{...({
|
|
144
|
+
className: 'text-[13px] leading-[18px] text-muted-foreground mt-1',
|
|
145
|
+
} as Record<string, string>)}
|
|
146
|
+
>
|
|
147
|
+
{description}
|
|
148
|
+
</Text>
|
|
149
|
+
) : null}
|
|
150
|
+
</View>
|
|
151
|
+
</Pressable>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const LinkPreviewCard = memo(LinkPreviewCardComponent);
|
|
156
|
+
LinkPreviewCard.displayName = 'LinkPreviewCard';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Props for {@link LinkPreviewCard}.
|
|
5
|
+
*
|
|
6
|
+
* Structural by design — Bloom does NOT depend on `@oxyhq/contracts`. The shape
|
|
7
|
+
* mirrors the subset of the Oxy SDK's `LinkPreview` DTO (`@oxyhq/core`
|
|
8
|
+
* `getLinkPreview` / `getLinkPreviews`) that is needed to render a preview, so a
|
|
9
|
+
* consumer can spread an SDK `LinkPreview` straight onto this component.
|
|
10
|
+
*/
|
|
11
|
+
export interface LinkPreviewCardProps {
|
|
12
|
+
/** The previewed URL. Opened via `Linking.openURL` when `onPress` is omitted. */
|
|
13
|
+
url: string;
|
|
14
|
+
/** OpenGraph / page title. Falls back to the URL hostname when absent. */
|
|
15
|
+
title?: string;
|
|
16
|
+
/** OpenGraph / meta description. */
|
|
17
|
+
description?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Absolute cover-image URL (e.g. a `cloud.oxy.so` OG image). Rendered directly
|
|
20
|
+
* with the RN `Image` primitive — NOT through `ImageResolver` / `Avatar`, which
|
|
21
|
+
* are for bare Oxy file IDs.
|
|
22
|
+
*/
|
|
23
|
+
image?: string;
|
|
24
|
+
/** Site / brand name shown above the title. Falls back to the URL hostname. */
|
|
25
|
+
siteName?: string;
|
|
26
|
+
/** Press handler. When omitted, the card opens `url` via `Linking.openURL`. */
|
|
27
|
+
onPress?: () => void;
|
|
28
|
+
/** Extra NativeWind utilities merged onto the card surface. */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Extra style merged onto the card surface. */
|
|
31
|
+
style?: StyleProp<ViewStyle>;
|
|
32
|
+
}
|