@oxyhq/bloom 0.23.0 → 0.24.1

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 (47) hide show
  1. package/lib/commonjs/index.js +20 -12
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/index.web.js +20 -12
  4. package/lib/commonjs/index.web.js.map +1 -1
  5. package/lib/commonjs/link-preview/LinkPreviewCard.js +141 -0
  6. package/lib/commonjs/link-preview/LinkPreviewCard.js.map +1 -0
  7. package/lib/commonjs/link-preview/index.js +13 -0
  8. package/lib/commonjs/link-preview/index.js.map +1 -0
  9. package/lib/commonjs/link-preview/types.js +6 -0
  10. package/lib/commonjs/link-preview/types.js.map +1 -0
  11. package/lib/module/index.js +1 -1
  12. package/lib/module/index.js.map +1 -1
  13. package/lib/module/index.web.js +1 -1
  14. package/lib/module/index.web.js.map +1 -1
  15. package/lib/module/link-preview/LinkPreviewCard.js +135 -0
  16. package/lib/module/link-preview/LinkPreviewCard.js.map +1 -0
  17. package/lib/module/link-preview/index.js +4 -0
  18. package/lib/module/link-preview/index.js.map +1 -0
  19. package/lib/module/link-preview/types.js +4 -0
  20. package/lib/module/link-preview/types.js.map +1 -0
  21. package/lib/typescript/commonjs/index.d.ts +2 -0
  22. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/index.web.d.ts +2 -0
  24. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  25. package/lib/typescript/commonjs/link-preview/LinkPreviewCard.d.ts +24 -0
  26. package/lib/typescript/commonjs/link-preview/LinkPreviewCard.d.ts.map +1 -0
  27. package/lib/typescript/commonjs/link-preview/index.d.ts +3 -0
  28. package/lib/typescript/commonjs/link-preview/index.d.ts.map +1 -0
  29. package/lib/typescript/commonjs/link-preview/types.d.ts +44 -0
  30. package/lib/typescript/commonjs/link-preview/types.d.ts.map +1 -0
  31. package/lib/typescript/module/index.d.ts +2 -0
  32. package/lib/typescript/module/index.d.ts.map +1 -1
  33. package/lib/typescript/module/index.web.d.ts +2 -0
  34. package/lib/typescript/module/index.web.d.ts.map +1 -1
  35. package/lib/typescript/module/link-preview/LinkPreviewCard.d.ts +24 -0
  36. package/lib/typescript/module/link-preview/LinkPreviewCard.d.ts.map +1 -0
  37. package/lib/typescript/module/link-preview/index.d.ts +3 -0
  38. package/lib/typescript/module/link-preview/index.d.ts.map +1 -0
  39. package/lib/typescript/module/link-preview/types.d.ts +44 -0
  40. package/lib/typescript/module/link-preview/types.d.ts.map +1 -0
  41. package/package.json +18 -1
  42. package/src/__tests__/LinkPreviewCard.test.tsx +88 -0
  43. package/src/index.ts +2 -0
  44. package/src/index.web.ts +2 -0
  45. package/src/link-preview/LinkPreviewCard.tsx +160 -0
  46. package/src/link-preview/index.ts +2 -0
  47. package/src/link-preview/types.ts +44 -0
@@ -0,0 +1,160 @@
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
+ coverFill = false,
60
+ onPress,
61
+ className,
62
+ style,
63
+ }) => {
64
+ const { colors } = useTheme();
65
+ const { state: pressed, onIn: onPressIn, onOut: onPressOut } =
66
+ useInteractionState();
67
+
68
+ const { displaySiteName, displayTitle } = useMemo(() => {
69
+ const host = hostnameOf(url);
70
+ return {
71
+ displaySiteName: siteName ?? host,
72
+ displayTitle: title ?? host ?? url,
73
+ };
74
+ }, [url, siteName, title]);
75
+
76
+ const handlePress = useCallback(() => {
77
+ if (onPress) {
78
+ onPress();
79
+ return;
80
+ }
81
+ // No explicit handler: open the URL with the system handler. A rejected
82
+ // promise (unsupported scheme / no handler) is non-actionable for a
83
+ // presentational card, so it is swallowed rather than surfaced.
84
+ void Linking.openURL(url).catch(() => undefined);
85
+ }, [onPress, url]);
86
+
87
+ const surfaceClass = ['overflow-hidden border border-border bg-card', className]
88
+ .filter(Boolean)
89
+ .join(' ');
90
+
91
+ return (
92
+ <Pressable
93
+ {...({ className: surfaceClass } as Record<string, string>)}
94
+ style={[
95
+ { borderRadius: RADIUS['radius-20'] },
96
+ pressed && { opacity: PRESSED_OPACITY },
97
+ style,
98
+ ]}
99
+ onPress={handlePress}
100
+ onPressIn={onPressIn}
101
+ onPressOut={onPressOut}
102
+ accessibilityRole="link"
103
+ accessibilityLabel={displayTitle}
104
+ >
105
+ {image ? (
106
+ <Image
107
+ source={{ uri: image }}
108
+ resizeMode="cover"
109
+ {...({ className: 'w-full' } as Record<string, string>)}
110
+ style={{
111
+ // Fill-height mode: flex into the card's bounding height (supplied
112
+ // by the consumer via `style`), letting the text block stay a
113
+ // compact footer. Default mode: fixed intrinsic cover height.
114
+ ...(coverFill ? { flex: 1 } : { height: COVER_IMAGE_HEIGHT }),
115
+ backgroundColor: colors.backgroundTertiary,
116
+ }}
117
+ />
118
+ ) : null}
119
+
120
+ <View {...({ className: 'p-3' } as Record<string, string>)}>
121
+ {displaySiteName ? (
122
+ <Text
123
+ numberOfLines={1}
124
+ {...({
125
+ className:
126
+ 'text-xs font-medium uppercase tracking-wide text-muted-foreground',
127
+ } as Record<string, string>)}
128
+ >
129
+ {displaySiteName}
130
+ </Text>
131
+ ) : null}
132
+
133
+ {displayTitle ? (
134
+ <Text
135
+ numberOfLines={2}
136
+ {...({
137
+ className: 'text-[15px] font-semibold text-foreground mt-1',
138
+ } as Record<string, string>)}
139
+ >
140
+ {displayTitle}
141
+ </Text>
142
+ ) : null}
143
+
144
+ {description ? (
145
+ <Text
146
+ numberOfLines={2}
147
+ {...({
148
+ className: 'text-[13px] leading-[18px] text-muted-foreground mt-1',
149
+ } as Record<string, string>)}
150
+ >
151
+ {description}
152
+ </Text>
153
+ ) : null}
154
+ </View>
155
+ </Pressable>
156
+ );
157
+ };
158
+
159
+ export const LinkPreviewCard = memo(LinkPreviewCardComponent);
160
+ LinkPreviewCard.displayName = 'LinkPreviewCard';
@@ -0,0 +1,2 @@
1
+ export { LinkPreviewCard } from './LinkPreviewCard';
2
+ export type { LinkPreviewCardProps } from './types';
@@ -0,0 +1,44 @@
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
+ /**
27
+ * Fill-height mode for the cover image. Defaults to `false`.
28
+ *
29
+ * - `false` (default): the cover image keeps its fixed intrinsic height —
30
+ * correct when the card sizes itself (e.g. a link rendered alone).
31
+ * - `true`: the cover image flexes (`flex: 1`, no fixed height) to fill
32
+ * whatever vertical space the card is given, with the text block as a
33
+ * compact intrinsic-height footer below it. The consumer supplies the
34
+ * bounding height via `style` (e.g. `style={{ height: 180 }}`) so the card
35
+ * fits a fixed-height row alongside other attachments without overflowing.
36
+ */
37
+ coverFill?: boolean;
38
+ /** Press handler. When omitted, the card opens `url` via `Linking.openURL`. */
39
+ onPress?: () => void;
40
+ /** Extra NativeWind utilities merged onto the card surface. */
41
+ className?: string;
42
+ /** Extra style merged onto the card surface. */
43
+ style?: StyleProp<ViewStyle>;
44
+ }