@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.
Files changed (71) hide show
  1. package/lib/commonjs/content-panel/index.js +12 -6
  2. package/lib/commonjs/content-panel/index.js.map +1 -1
  3. package/lib/commonjs/content-panel/index.web.js +31 -21
  4. package/lib/commonjs/content-panel/index.web.js.map +1 -1
  5. package/lib/commonjs/content-panel/nesting-context.js +39 -0
  6. package/lib/commonjs/content-panel/nesting-context.js.map +1 -0
  7. package/lib/commonjs/index.js +20 -12
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/index.web.js +20 -12
  10. package/lib/commonjs/index.web.js.map +1 -1
  11. package/lib/commonjs/link-preview/LinkPreviewCard.js +133 -0
  12. package/lib/commonjs/link-preview/LinkPreviewCard.js.map +1 -0
  13. package/lib/commonjs/link-preview/index.js +13 -0
  14. package/lib/commonjs/link-preview/index.js.map +1 -0
  15. package/lib/commonjs/link-preview/types.js +6 -0
  16. package/lib/commonjs/link-preview/types.js.map +1 -0
  17. package/lib/module/content-panel/index.js +12 -6
  18. package/lib/module/content-panel/index.js.map +1 -1
  19. package/lib/module/content-panel/index.web.js +31 -21
  20. package/lib/module/content-panel/index.web.js.map +1 -1
  21. package/lib/module/content-panel/nesting-context.js +35 -0
  22. package/lib/module/content-panel/nesting-context.js.map +1 -0
  23. package/lib/module/index.js +1 -1
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/module/index.web.js +1 -1
  26. package/lib/module/index.web.js.map +1 -1
  27. package/lib/module/link-preview/LinkPreviewCard.js +127 -0
  28. package/lib/module/link-preview/LinkPreviewCard.js.map +1 -0
  29. package/lib/module/link-preview/index.js +4 -0
  30. package/lib/module/link-preview/index.js.map +1 -0
  31. package/lib/module/link-preview/types.js +4 -0
  32. package/lib/module/link-preview/types.js.map +1 -0
  33. package/lib/typescript/commonjs/content-panel/index.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/content-panel/index.web.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/content-panel/nesting-context.d.ts +24 -0
  36. package/lib/typescript/commonjs/content-panel/nesting-context.d.ts.map +1 -0
  37. package/lib/typescript/commonjs/index.d.ts +2 -0
  38. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/index.web.d.ts +2 -0
  40. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/link-preview/LinkPreviewCard.d.ts +24 -0
  42. package/lib/typescript/commonjs/link-preview/LinkPreviewCard.d.ts.map +1 -0
  43. package/lib/typescript/commonjs/link-preview/index.d.ts +3 -0
  44. package/lib/typescript/commonjs/link-preview/index.d.ts.map +1 -0
  45. package/lib/typescript/commonjs/link-preview/types.d.ts +32 -0
  46. package/lib/typescript/commonjs/link-preview/types.d.ts.map +1 -0
  47. package/lib/typescript/module/content-panel/index.d.ts.map +1 -1
  48. package/lib/typescript/module/content-panel/index.web.d.ts.map +1 -1
  49. package/lib/typescript/module/content-panel/nesting-context.d.ts +24 -0
  50. package/lib/typescript/module/content-panel/nesting-context.d.ts.map +1 -0
  51. package/lib/typescript/module/index.d.ts +2 -0
  52. package/lib/typescript/module/index.d.ts.map +1 -1
  53. package/lib/typescript/module/index.web.d.ts +2 -0
  54. package/lib/typescript/module/index.web.d.ts.map +1 -1
  55. package/lib/typescript/module/link-preview/LinkPreviewCard.d.ts +24 -0
  56. package/lib/typescript/module/link-preview/LinkPreviewCard.d.ts.map +1 -0
  57. package/lib/typescript/module/link-preview/index.d.ts +3 -0
  58. package/lib/typescript/module/link-preview/index.d.ts.map +1 -0
  59. package/lib/typescript/module/link-preview/types.d.ts +32 -0
  60. package/lib/typescript/module/link-preview/types.d.ts.map +1 -0
  61. package/package.json +18 -1
  62. package/src/__tests__/ContentPanelNesting.test.tsx +52 -0
  63. package/src/__tests__/LinkPreviewCard.test.tsx +60 -0
  64. package/src/content-panel/index.tsx +18 -8
  65. package/src/content-panel/index.web.tsx +40 -29
  66. package/src/content-panel/nesting-context.ts +35 -0
  67. package/src/index.ts +2 -0
  68. package/src/index.web.ts +2 -0
  69. package/src/link-preview/LinkPreviewCard.tsx +156 -0
  70. package/src/link-preview/index.ts +2 -0
  71. 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,2 @@
1
+ export { LinkPreviewCard } from './LinkPreviewCard';
2
+ export type { LinkPreviewCardProps } from './types';
@@ -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
+ }