@streamplace/components 0.9.1 → 0.9.6

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 (70) hide show
  1. package/dist/components/chat/mod-view.d.ts.map +1 -1
  2. package/dist/components/chat/mod-view.js +8 -88
  3. package/dist/components/chat/mod-view.js.map +1 -1
  4. package/dist/components/chat/update-stream-title-dialog.d.ts +9 -0
  5. package/dist/components/chat/update-stream-title-dialog.d.ts.map +1 -0
  6. package/dist/components/chat/update-stream-title-dialog.js +74 -0
  7. package/dist/components/chat/update-stream-title-dialog.js.map +1 -0
  8. package/dist/components/content-metadata/content-metadata-form.d.ts.map +1 -1
  9. package/dist/components/content-metadata/content-metadata-form.js +7 -8
  10. package/dist/components/content-metadata/content-metadata-form.js.map +1 -1
  11. package/dist/components/mobile-player/ui/report-modal.d.ts.map +1 -1
  12. package/dist/components/mobile-player/ui/report-modal.js +1 -1
  13. package/dist/components/mobile-player/ui/report-modal.js.map +1 -1
  14. package/dist/components/mobile-player/ui/viewer-loading-overlay.d.ts.map +1 -1
  15. package/dist/components/mobile-player/ui/viewer-loading-overlay.js +0 -1
  16. package/dist/components/mobile-player/ui/viewer-loading-overlay.js.map +1 -1
  17. package/dist/components/ui/admonition.d.ts +14 -0
  18. package/dist/components/ui/admonition.d.ts.map +1 -0
  19. package/dist/components/ui/admonition.js +117 -0
  20. package/dist/components/ui/admonition.js.map +1 -0
  21. package/dist/components/ui/button.d.ts +1 -0
  22. package/dist/components/ui/button.d.ts.map +1 -1
  23. package/dist/components/ui/button.js +2 -2
  24. package/dist/components/ui/button.js.map +1 -1
  25. package/dist/components/ui/index.d.ts +1 -0
  26. package/dist/components/ui/index.d.ts.map +1 -1
  27. package/dist/components/ui/index.js +1 -0
  28. package/dist/components/ui/index.js.map +1 -1
  29. package/dist/components/ui/primitives/button.d.ts +3 -2
  30. package/dist/components/ui/primitives/button.d.ts.map +1 -1
  31. package/dist/components/ui/primitives/button.js +20 -2
  32. package/dist/components/ui/primitives/button.js.map +1 -1
  33. package/dist/components/ui/resizeable.d.ts.map +1 -1
  34. package/dist/components/ui/resizeable.js +2 -1
  35. package/dist/components/ui/resizeable.js.map +1 -1
  36. package/dist/components/ui/text.d.ts +2 -1
  37. package/dist/components/ui/text.d.ts.map +1 -1
  38. package/dist/components/ui/text.js.map +1 -1
  39. package/dist/components/ui/textarea.d.ts.map +1 -1
  40. package/dist/components/ui/textarea.js +3 -1
  41. package/dist/components/ui/textarea.js.map +1 -1
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +2 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/lib/theme/atoms.d.ts +148 -148
  47. package/dist/lib/theme/tokens.d.ts +11 -11
  48. package/dist/lib/theme/tokens.js +11 -11
  49. package/dist/utils/did.d.ts +13 -0
  50. package/dist/utils/did.d.ts.map +1 -0
  51. package/dist/utils/did.js +43 -0
  52. package/dist/utils/did.js.map +1 -0
  53. package/locales/en-US/settings.ftl +2 -1
  54. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  55. package/package.json +2 -2
  56. package/src/components/chat/mod-view.tsx +2 -218
  57. package/src/components/chat/update-stream-title-dialog.tsx +169 -0
  58. package/src/components/content-metadata/content-metadata-form.tsx +37 -10
  59. package/src/components/mobile-player/ui/report-modal.tsx +2 -0
  60. package/src/components/mobile-player/ui/viewer-loading-overlay.tsx +0 -1
  61. package/src/components/ui/admonition.tsx +177 -0
  62. package/src/components/ui/button.tsx +3 -0
  63. package/src/components/ui/index.ts +1 -0
  64. package/src/components/ui/primitives/button.tsx +37 -11
  65. package/src/components/ui/resizeable.tsx +2 -1
  66. package/src/components/ui/text.tsx +11 -1
  67. package/src/components/ui/textarea.tsx +3 -0
  68. package/src/index.tsx +2 -0
  69. package/src/lib/theme/tokens.ts +11 -11
  70. package/src/utils/did.ts +61 -0
@@ -1,10 +1,11 @@
1
1
  import { forwardRef, useCallback, useEffect, useState } from "react";
2
- import { ScrollView, View } from "react-native";
2
+ import { Linking, Pressable, ScrollView, View } from "react-native";
3
3
  import {
4
4
  CONTENT_WARNINGS,
5
5
  LICENSE_OPTIONS,
6
6
  } from "../../lib/metadata-constants";
7
7
 
8
+ import { ExternalLink } from "lucide-react-native";
8
9
  import {
9
10
  PlaceStreamMetadataConfiguration,
10
11
  PlaceStreamMetadataContentRights,
@@ -21,6 +22,7 @@ import {
21
22
  } from "../../streamplace-store/streamplace-store";
22
23
  import { usePDSAgent } from "../../streamplace-store/xrpc";
23
24
  import * as zero from "../../ui";
25
+ import { Admonition } from "../ui";
24
26
  import { Button } from "../ui/button";
25
27
  import { Checkbox } from "../ui/checkbox";
26
28
  import { Input } from "../ui/input";
@@ -41,7 +43,6 @@ export interface ContentMetadataFormProps {
41
43
  style?: any;
42
44
  }
43
45
 
44
- // ButtonSelector component (same as in livestream-panel)
45
46
  const ButtonSelector = ({
46
47
  values,
47
48
  selectedValue,
@@ -55,12 +56,13 @@ const ButtonSelector = ({
55
56
  disabledValues?: string[];
56
57
  style?: any[];
57
58
  }) => (
58
- <View style={[layout.flex.row, gap.all[1], ...style]}>
59
+ <View style={[layout.flex.row, gap.all[1], layout.flex.wrap.wrap, ...style]}>
59
60
  {values.map(({ label, value }) => (
60
61
  <Button
61
62
  key={value}
62
63
  variant={selectedValue === value ? "primary" : "secondary"}
63
64
  size="pill"
65
+ width="min"
64
66
  disabled={disabledValues.includes(value)}
65
67
  onPress={() => setSelectedValue(value)}
66
68
  style={[
@@ -71,10 +73,8 @@ const ButtonSelector = ({
71
73
  ]}
72
74
  >
73
75
  <Text
74
- style={[
75
- selectedValue === value ? text.white : text.gray[300],
76
- { fontSize: 14, fontWeight: "600" },
77
- ]}
76
+ size="sm"
77
+ style={[selectedValue === value ? text.white : text.gray[300]]}
78
78
  >
79
79
  {label}
80
80
  </Text>
@@ -93,6 +93,7 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
93
93
  const getContentMetadata = useGetContentMetadata();
94
94
  const saveContentMetadata = useSaveContentMetadata();
95
95
  const toast = useToast();
96
+ const th = zero.useTheme();
96
97
 
97
98
  // Local state for metadata
98
99
  const [contentWarnings, setContentWarnings] = useState<string[]>([]);
@@ -364,7 +365,7 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
364
365
  ]}
365
366
  selectedValue={activeSection}
366
367
  setSelectedValue={setActiveSection}
367
- style={[{ marginVertical: -2, flexDirection: "column" }]}
368
+ style={[{ marginVertical: -2 }]}
368
369
  />
369
370
  </View>
370
371
 
@@ -379,8 +380,7 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
379
380
  gap.all[2],
380
381
  ]}
381
382
  >
382
- <Text>Content Warnings</Text>
383
- <Text muted>(optional)</Text>
383
+ <Text size="lg">Content Warnings</Text>
384
384
  </View>
385
385
  <View style={[gap.all[2], w.percent[100]]}>
386
386
  {CONTENT_WARNINGS.map((warning) => (
@@ -398,6 +398,33 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
398
398
  </View>
399
399
  ))}
400
400
  </View>
401
+ <Admonition variant="info" size="sm">
402
+ <Text size="sm">
403
+ You are required to disclose if your content is not suitable
404
+ for certain viewers.
405
+ </Text>
406
+ </Admonition>
407
+ <Admonition variant="warning" size="sm">
408
+ <Text size="sm">
409
+ Your node may prohibit some of this content. Read the
410
+ community guidelines to make sure.{" "}
411
+ <Pressable
412
+ onPress={() =>
413
+ Linking.openURL(
414
+ "https://blog.stream.place/3mcqwibo4ks2w",
415
+ )
416
+ }
417
+ >
418
+ <Text size="sm" color={zero.colors.blue[400]}>
419
+ Learn more{" "}
420
+ <ExternalLink
421
+ size="14"
422
+ style={{ marginVertical: -2 }}
423
+ />
424
+ </Text>
425
+ </Pressable>
426
+ </Text>
427
+ </Admonition>
401
428
  </View>
402
429
  )}
403
430
 
@@ -173,6 +173,7 @@ export const ReportModal: React.FC<ReportModalProps> = ({
173
173
  </View>
174
174
  <DialogFooter>
175
175
  <Button
176
+ width="min"
176
177
  variant="secondary"
177
178
  onPress={handleCancel}
178
179
  disabled={isSubmitting}
@@ -180,6 +181,7 @@ export const ReportModal: React.FC<ReportModalProps> = ({
180
181
  <Text>Cancel</Text>
181
182
  </Button>
182
183
  <Button
184
+ width="min"
183
185
  variant="primary"
184
186
  onPress={handleSubmit}
185
187
  disabled={!selectedReason || isSubmitting}
@@ -52,7 +52,6 @@ export function ViewerLoadingOverlay() {
52
52
  position: "absolute",
53
53
  width: "100%",
54
54
  height: "100%",
55
- zIndex: 998,
56
55
  alignItems: "center",
57
56
  justifyContent: "center",
58
57
  backgroundColor: "rgba(0,0,0,0.3)",
@@ -0,0 +1,177 @@
1
+ import { AlertCircle, CheckCircle, Info, XCircle } from "lucide-react-native";
2
+ import { View, ViewStyle } from "react-native";
3
+ import { useTheme } from "../../ui";
4
+ import { Text } from "./text";
5
+
6
+ type AdmonitionVariant = "default" | "success" | "error" | "info" | "warning";
7
+ type AdmonitionSize = "sm" | "md" | "lg";
8
+
9
+ type AdmonitionProps = {
10
+ variant?: AdmonitionVariant;
11
+ size?: AdmonitionSize;
12
+ title?: string;
13
+ children?: React.ReactNode;
14
+ iconLeft?: React.ComponentType<any>;
15
+ style?: ViewStyle;
16
+ };
17
+
18
+ export function Admonition({
19
+ variant = "default",
20
+ size = "md",
21
+ title,
22
+ children,
23
+ iconLeft,
24
+ style,
25
+ }: AdmonitionProps) {
26
+ const { theme, icons } = useTheme();
27
+
28
+ const defaultIconLeft = (() => {
29
+ if (iconLeft) return iconLeft;
30
+ switch (variant) {
31
+ case "success":
32
+ return CheckCircle;
33
+ case "error":
34
+ return XCircle;
35
+ case "info":
36
+ return Info;
37
+ case "warning":
38
+ return AlertCircle;
39
+ default:
40
+ return Info;
41
+ }
42
+ })();
43
+
44
+ const FinalIconLeft = defaultIconLeft;
45
+
46
+ const variantStyles: Record<AdmonitionVariant, ViewStyle> = {
47
+ default: {
48
+ backgroundColor: theme.colors.secondary,
49
+ borderColor: theme.colors.border,
50
+ },
51
+ success: {
52
+ backgroundColor: theme.colors.success + "15",
53
+ borderColor: theme.colors.success,
54
+ },
55
+ error: {
56
+ backgroundColor: theme.colors.destructive + "15",
57
+ borderColor: theme.colors.destructive,
58
+ },
59
+ info: {
60
+ backgroundColor: theme.colors.info + "15",
61
+ borderColor: theme.colors.info,
62
+ },
63
+ warning: {
64
+ backgroundColor: theme.colors.warning + "15",
65
+ borderColor: theme.colors.warning,
66
+ },
67
+ };
68
+
69
+ const iconColor = (() => {
70
+ switch (variant) {
71
+ case "success":
72
+ return theme.colors.success;
73
+ case "error":
74
+ return theme.colors.destructive;
75
+ case "info":
76
+ return theme.colors.info;
77
+ case "warning":
78
+ return theme.colors.warning;
79
+ default:
80
+ return theme.colors.foreground;
81
+ }
82
+ })();
83
+
84
+ const sizeConfig = (() => {
85
+ switch (size) {
86
+ case "sm":
87
+ return {
88
+ borderRadius: 8,
89
+ padding: 12,
90
+ gap: 6,
91
+ iconSize: icons.size.md,
92
+ titleSize: "base" as const,
93
+ contentSize: "sm" as const,
94
+ innerGap: 8,
95
+ };
96
+ case "lg":
97
+ return {
98
+ borderRadius: 16,
99
+ padding: 20,
100
+ gap: 12,
101
+ iconSize: icons.size.xl,
102
+ titleSize: "xl" as const,
103
+ contentSize: "lg" as const,
104
+ innerGap: 16,
105
+ };
106
+ case "md":
107
+ default:
108
+ return {
109
+ borderRadius: 12,
110
+ padding: 16,
111
+ gap: 8,
112
+ iconSize: icons.size.lg,
113
+ titleSize: "lg" as const,
114
+ contentSize: "base" as const,
115
+ innerGap: 12,
116
+ };
117
+ }
118
+ })();
119
+
120
+ let childrenIn = (
121
+ <View
122
+ style={{
123
+ paddingLeft: title ? sizeConfig.iconSize + sizeConfig.innerGap : 0,
124
+ }}
125
+ >
126
+ {typeof children === "string" ? (
127
+ <Text
128
+ size={sizeConfig.contentSize}
129
+ style={{ color: theme.colors.cardForeground, flexWrap: "wrap" }}
130
+ >
131
+ {children}
132
+ </Text>
133
+ ) : (
134
+ children
135
+ )}
136
+ </View>
137
+ );
138
+
139
+ return (
140
+ <View
141
+ style={[
142
+ {
143
+ borderRadius: sizeConfig.borderRadius,
144
+ borderWidth: 1,
145
+ padding: sizeConfig.padding,
146
+ gap: sizeConfig.gap,
147
+ },
148
+ variantStyles[variant],
149
+ style,
150
+ ]}
151
+ >
152
+ <View
153
+ style={{
154
+ flexDirection: "row",
155
+ alignItems: "flex-start",
156
+ gap: sizeConfig.innerGap,
157
+ }}
158
+ >
159
+ {FinalIconLeft && (
160
+ <FinalIconLeft size={sizeConfig.iconSize} color={iconColor} />
161
+ )}
162
+ {title ? (
163
+ <Text
164
+ size={sizeConfig.titleSize}
165
+ weight="semibold"
166
+ style={{ flex: 1 }}
167
+ >
168
+ {title}
169
+ </Text>
170
+ ) : (
171
+ children && <View style={{ flex: 1 }}>{childrenIn}</View>
172
+ )}
173
+ </View>
174
+ {children && title && childrenIn}
175
+ </View>
176
+ );
177
+ }
@@ -41,6 +41,7 @@ export interface ButtonProps
41
41
  loading?: boolean;
42
42
  loadingText?: string;
43
43
  width?: "full" | "min" | number;
44
+ hoverStyle?: ButtonPrimitiveProps["hoverStyle"];
44
45
  }
45
46
 
46
47
  export const Button = forwardRef<any, ButtonProps>(
@@ -56,6 +57,7 @@ export const Button = forwardRef<any, ButtonProps>(
56
57
  disabled,
57
58
  style,
58
59
  width = "full",
60
+ hoverStyle,
59
61
  ...props
60
62
  },
61
63
  ref,
@@ -222,6 +224,7 @@ export const Button = forwardRef<any, ButtonProps>(
222
224
  ref={ref}
223
225
  disabled={disabled || loading}
224
226
  style={[buttonStyle, sizeStyles.button, widthStyle, style]}
227
+ hoverStyle={hoverStyle}
225
228
  {...props}
226
229
  >
227
230
  <ButtonPrimitive.Content style={sizeStyles.inner}>
@@ -5,6 +5,7 @@ export * from "./primitives/modal";
5
5
  export * from "./primitives/text";
6
6
 
7
7
  // Export styled components
8
+ export * from "./admonition";
8
9
  export * from "./button";
9
10
  export * from "./checkbox";
10
11
  export * from "./dialog";
@@ -1,19 +1,21 @@
1
- import React, { forwardRef } from "react";
1
+ import React, { forwardRef, useState } from "react";
2
2
  import {
3
3
  AccessibilityRole,
4
4
  GestureResponderEvent,
5
+ Platform,
6
+ Pressable,
7
+ PressableProps,
8
+ StyleProp,
5
9
  StyleSheet,
6
10
  Text,
7
11
  TextProps,
8
- TouchableOpacity,
9
- TouchableOpacityProps,
10
12
  View,
11
13
  ViewProps,
14
+ ViewStyle,
12
15
  } from "react-native";
13
16
 
14
17
  // Base button primitive interface
15
- export interface ButtonPrimitiveProps
16
- extends Omit<TouchableOpacityProps, "onPress"> {
18
+ export interface ButtonPrimitiveProps extends Omit<PressableProps, "onPress"> {
17
19
  onPress?: (event: GestureResponderEvent) => void;
18
20
  disabled?: boolean;
19
21
  loading?: boolean;
@@ -21,11 +23,12 @@ export interface ButtonPrimitiveProps
21
23
  accessibilityLabel?: string;
22
24
  accessibilityHint?: string;
23
25
  testID?: string;
26
+ hoverStyle?: StyleProp<ViewStyle>;
24
27
  }
25
28
 
26
29
  // Button root primitive - handles all touch interactions
27
30
  export const ButtonRoot = forwardRef<
28
- React.ComponentRef<typeof TouchableOpacity>,
31
+ React.ComponentRef<typeof Pressable>,
29
32
  ButtonPrimitiveProps
30
33
  >(
31
34
  (
@@ -43,11 +46,13 @@ export const ButtonRoot = forwardRef<
43
46
  accessibilityState,
44
47
  testID,
45
48
  style,
46
- activeOpacity = 0.7,
49
+ hoverStyle,
47
50
  ...props
48
51
  },
49
52
  ref,
50
53
  ) => {
54
+ const [isHovered, setIsHovered] = useState(false);
55
+
51
56
  const handlePress = React.useCallback(
52
57
  (event: GestureResponderEvent) => {
53
58
  if (!disabled && !loading && onPress) {
@@ -84,15 +89,26 @@ export const ButtonRoot = forwardRef<
84
89
  [disabled, loading, onLongPress],
85
90
  );
86
91
 
92
+ const handleHoverIn = React.useCallback(() => {
93
+ if (!disabled && !loading) {
94
+ setIsHovered(true);
95
+ }
96
+ }, [disabled, loading]);
97
+
98
+ const handleHoverOut = React.useCallback(() => {
99
+ setIsHovered(false);
100
+ }, []);
101
+
87
102
  return (
88
- <TouchableOpacity
103
+ <Pressable
89
104
  ref={ref}
90
105
  onPress={handlePress}
91
106
  onPressIn={handlePressIn}
92
107
  onPressOut={handlePressOut}
93
108
  onLongPress={handleLongPress}
109
+ onHoverIn={handleHoverIn}
110
+ onHoverOut={handleHoverOut}
94
111
  disabled={disabled || loading}
95
- activeOpacity={disabled || loading ? 1 : activeOpacity}
96
112
  accessibilityRole={accessibilityRole}
97
113
  accessibilityLabel={accessibilityLabel}
98
114
  accessibilityHint={accessibilityHint}
@@ -104,13 +120,15 @@ export const ButtonRoot = forwardRef<
104
120
  testID={testID}
105
121
  style={[
106
122
  primitiveStyles.button,
123
+ primitiveStyles.transition,
107
124
  (disabled || loading) && primitiveStyles.disabled,
108
- style,
125
+ style as any,
126
+ isHovered && hoverStyle,
109
127
  ]}
110
128
  {...props}
111
129
  >
112
130
  {children}
113
- </TouchableOpacity>
131
+ </Pressable>
114
132
  );
115
133
  },
116
134
  );
@@ -245,6 +263,14 @@ const primitiveStyles = StyleSheet.create({
245
263
  alignItems: "center",
246
264
  justifyContent: "center",
247
265
  },
266
+ transition:
267
+ Platform.OS === "web"
268
+ ? // probably fine if web-only
269
+ ({
270
+ transitionDuration: "150ms",
271
+ transitionProperty: "background-color, border-color, color",
272
+ } as any)
273
+ : undefined,
248
274
  disabled: {
249
275
  opacity: 0.5,
250
276
  },
@@ -95,7 +95,8 @@ export function Resizable({
95
95
  translateY:
96
96
  slideKeyboard +
97
97
  Math.max(0, -sheetHeight.value) +
98
- (slideKeyboard < 0 ? 0 : -safeBottom),
98
+ (slideKeyboard < 0 ? 0 : -safeBottom) -
99
+ (Math.abs(slideKeyboard) > 1 ? 32 : 16),
99
100
  },
100
101
  ],
101
102
  }));
@@ -62,7 +62,17 @@ const textVariants = cva("", {
62
62
 
63
63
  export interface TextProps
64
64
  extends Omit<TextPrimitiveProps, "variant" | "size" | "weight" | "color">,
65
- VariantProps<typeof textVariants> {
65
+ Omit<VariantProps<typeof textVariants>, "color"> {
66
+ // Override color to accept hex values and custom strings
67
+ color?:
68
+ | "default"
69
+ | "muted"
70
+ | "primary"
71
+ | "secondary"
72
+ | "destructive"
73
+ | "success"
74
+ | "warning"
75
+ | (string & {});
66
76
  // Additional convenience props
67
77
  muted?: boolean;
68
78
  bold?: boolean;
@@ -5,9 +5,11 @@ import {
5
5
  import * as React from "react";
6
6
  import { Platform, TextInput, type TextInputProps } from "react-native";
7
7
  import { bg, borders, flex, p, text } from "../../lib/theme/atoms";
8
+ import { useTheme } from "../../ui";
8
9
 
9
10
  const Textarea = React.forwardRef<TextInput, TextInputProps>(
10
11
  ({ style, multiline = true, numberOfLines = 4, ...props }, ref) => {
12
+ let th = useTheme();
11
13
  // Detect if we're inside a bottom sheet
12
14
  let isInBottomSheet = false;
13
15
  try {
@@ -41,6 +43,7 @@ const Textarea = React.forwardRef<TextInput, TextInputProps>(
41
43
  multiline={multiline}
42
44
  numberOfLines={numberOfLines}
43
45
  textAlignVertical="top"
46
+ placeholderTextColor={th.theme.colors.textMuted}
44
47
  {...props}
45
48
  />
46
49
  );
package/src/index.tsx CHANGED
@@ -34,12 +34,14 @@ export * from "./lib/theme";
34
34
  export * from "./components/chat/chat";
35
35
  export * from "./components/chat/chat-box";
36
36
  export * from "./components/chat/system-message";
37
+ export * from "./components/chat/update-stream-title-dialog";
37
38
  export { default as VideoRetry } from "./components/mobile-player/video-retry";
38
39
  export * from "./lib/system-messages";
39
40
 
40
41
  export * from "./components/stream-notification";
41
42
  export * from "./lib/stream-notifications";
42
43
 
44
+ export * from "./utils/did";
43
45
  export * from "./utils/format-handle";
44
46
 
45
47
  export { DanmuOverlay } from "./components/danmu/danmu-overlay";
@@ -337,17 +337,17 @@ export const colors = {
337
337
  },
338
338
 
339
339
  warning: {
340
- 50: "#fffbeb",
341
- 100: "#fef3c7",
342
- 200: "#fde68a",
343
- 300: "#fcd34d",
344
- 400: "#fbbf24",
345
- 500: "#f59e0b",
346
- 600: "#d97706",
347
- 700: "#b45309",
348
- 800: "#92400e",
349
- 900: "#78350f",
350
- 950: "#451a03",
340
+ 50: "#fffaf0",
341
+ 100: "#ffe6c7",
342
+ 200: "#ffd99e",
343
+ 300: "#ffcc75",
344
+ 400: "#ffb94e",
345
+ 500: "#ff9e1f",
346
+ 600: "#e67e00",
347
+ 700: "#cc6600",
348
+ 800: "#998c00",
349
+ 900: "#664200",
350
+ 950: "#332900",
351
351
  },
352
352
 
353
353
  // iOS system colors (adaptive)
@@ -0,0 +1,61 @@
1
+ export interface DIDDocument {
2
+ id: string;
3
+ service?: Array<{
4
+ id: string;
5
+ type?: string;
6
+ serviceEndpoint?: string;
7
+ }>;
8
+ [key: string]: any;
9
+ }
10
+
11
+ export async function resolveDIDDocument(did: string): Promise<DIDDocument> {
12
+ let didDocUrl: string;
13
+
14
+ if (did.startsWith("did:web:")) {
15
+ // For did:web, construct the URL directly
16
+ const domain = did.replace("did:web:", "").replace(/:/g, "/");
17
+ didDocUrl = `https://${domain}/.well-known/did.json`;
18
+ } else if (did.startsWith("did:plc:")) {
19
+ // For did:plc, use plc.directory
20
+ didDocUrl = `https://plc.directory/${did}`;
21
+ } else {
22
+ throw new Error(`Unsupported DID method: ${did}`);
23
+ }
24
+
25
+ const response = await fetch(didDocUrl);
26
+ if (!response.ok) {
27
+ throw new Error(
28
+ `Failed to resolve DID document for ${did}: ${response.status}`,
29
+ );
30
+ }
31
+
32
+ return response.json();
33
+ }
34
+
35
+ export function getPDSServiceEndpoint(didDoc: DIDDocument): string {
36
+ const pdsService = didDoc.service?.find((s) => s.id === "#atproto_pds");
37
+
38
+ if (!pdsService?.serviceEndpoint) {
39
+ throw new Error("No PDS service endpoint found in DID document");
40
+ }
41
+
42
+ return pdsService.serviceEndpoint;
43
+ }
44
+
45
+ export async function getBlob(
46
+ did: string,
47
+ cid: string,
48
+ didDoc?: DIDDocument,
49
+ ): Promise<Blob> {
50
+ const doc = didDoc || (await resolveDIDDocument(did));
51
+ const pdsEndpoint = getPDSServiceEndpoint(doc);
52
+
53
+ const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`;
54
+
55
+ const response = await fetch(blobUrl);
56
+ if (!response.ok) {
57
+ throw new Error(`Failed to fetch blob: ${response.status}`);
58
+ }
59
+
60
+ return response.blob();
61
+ }