linked-data-browser 0.0.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 (91) hide show
  1. package/.eslintrc.js +13 -0
  2. package/.ldo/profile.context.ts +459 -0
  3. package/.ldo/profile.schema.ts +751 -0
  4. package/.ldo/profile.shapeTypes.ts +71 -0
  5. package/.ldo/profile.typings.ts +295 -0
  6. package/.prettierignore +6 -0
  7. package/.prettierrc +10 -0
  8. package/.shapes/profile.shex +121 -0
  9. package/README.md +3 -0
  10. package/app/index.tsx +25 -0
  11. package/app.json +37 -0
  12. package/assets/images/adaptive-icon.png +0 -0
  13. package/assets/images/favicon.png +0 -0
  14. package/assets/images/icon.png +0 -0
  15. package/assets/images/splash.png +0 -0
  16. package/babel.config.js +6 -0
  17. package/components/DataBrowser.tsx +57 -0
  18. package/components/ResourceView.tsx +14 -0
  19. package/components/TargetResourceProvider.tsx +128 -0
  20. package/components/ThemeProvider.tsx +123 -0
  21. package/components/nav/DialogProvider.tsx +140 -0
  22. package/components/nav/Layout.tsx +118 -0
  23. package/components/nav/header/AddressBox.tsx +126 -0
  24. package/components/nav/header/AvatarMenu.tsx +62 -0
  25. package/components/nav/header/Header.tsx +28 -0
  26. package/components/nav/header/SignInMenu.tsx +126 -0
  27. package/components/nav/header/ThemeToggleMenu.tsx +34 -0
  28. package/components/nav/header/ViewMenu.tsx +88 -0
  29. package/components/nav/useValidView.tsx +51 -0
  30. package/components/nav/utilityResourceViews/ErrorMessageResourceView.tsx +26 -0
  31. package/components/ui/avatar.tsx +53 -0
  32. package/components/ui/button.tsx +88 -0
  33. package/components/ui/card.tsx +101 -0
  34. package/components/ui/dialog.tsx +159 -0
  35. package/components/ui/dropdown-menu.tsx +275 -0
  36. package/components/ui/input.tsx +25 -0
  37. package/components/ui/label.tsx +34 -0
  38. package/components/ui/navigation-menu.tsx +200 -0
  39. package/components/ui/popover.tsx +45 -0
  40. package/components/ui/progress.tsx +79 -0
  41. package/components/ui/radio-group.tsx +59 -0
  42. package/components/ui/separator.tsx +27 -0
  43. package/components/ui/switch.tsx +105 -0
  44. package/components/ui/text.tsx +30 -0
  45. package/components/ui/tooltip.tsx +46 -0
  46. package/components.json +7 -0
  47. package/global.css +61 -0
  48. package/index.js +12 -0
  49. package/lib/android-navigation-bar.ts +11 -0
  50. package/lib/constants.ts +18 -0
  51. package/lib/icons/ArrowRight.tsx +4 -0
  52. package/lib/icons/Check.tsx +4 -0
  53. package/lib/icons/ChevronDown.tsx +4 -0
  54. package/lib/icons/ChevronRight.tsx +4 -0
  55. package/lib/icons/ChevronUp.tsx +4 -0
  56. package/lib/icons/ChevronsRight.tsx +4 -0
  57. package/lib/icons/CircleSlash.tsx +4 -0
  58. package/lib/icons/CircleX.tsx +4 -0
  59. package/lib/icons/Code.tsx +4 -0
  60. package/lib/icons/EllipsisVertical.tsx +4 -0
  61. package/lib/icons/EyeOff.tsx +4 -0
  62. package/lib/icons/File.tsx +4 -0
  63. package/lib/icons/Folder.tsx +4 -0
  64. package/lib/icons/Folders.tsx +4 -0
  65. package/lib/icons/Info.tsx +4 -0
  66. package/lib/icons/MonitorSmartphone.tsx +4 -0
  67. package/lib/icons/MoonStar.tsx +4 -0
  68. package/lib/icons/OctagonX.tsx +4 -0
  69. package/lib/icons/RefreshCw.tsx +4 -0
  70. package/lib/icons/ShieldX.tsx +4 -0
  71. package/lib/icons/Sun.tsx +4 -0
  72. package/lib/icons/TextCursorInput.tsx +4 -0
  73. package/lib/icons/Trash.tsx +4 -0
  74. package/lib/icons/ViewIcon.tsx +4 -0
  75. package/lib/icons/X.tsx +4 -0
  76. package/lib/icons/iconWithClassName.ts +14 -0
  77. package/lib/utils.ts +6 -0
  78. package/metro.config.js +6 -0
  79. package/nativewind-env.d.ts +1 -0
  80. package/package.json +89 -0
  81. package/resourceViews/Container/ContainerConfig.tsx +13 -0
  82. package/resourceViews/Container/ContainerView.tsx +148 -0
  83. package/resourceViews/RawCode/RawCodeConfig.tsx +11 -0
  84. package/resourceViews/RawCode/RawCodeEditor.tsx +37 -0
  85. package/resourceViews/RawCode/RawCodeView.tsx +67 -0
  86. package/scripts/adjust-server-paths.js +37 -0
  87. package/scripts/adjust-standalone-paths.js +28 -0
  88. package/tailwind.config.js +69 -0
  89. package/test-server/server-config.json +75 -0
  90. package/test-server/solid-css-seed.json +11 -0
  91. package/tsconfig.json +19 -0
@@ -0,0 +1,128 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ FunctionComponent,
4
+ PropsWithChildren,
5
+ createContext,
6
+ useContext,
7
+ } from 'react';
8
+ import { SolidLeaf, SolidContainer } from '@ldo/connected-solid';
9
+ import { InvalidIdentifierResource } from '@ldo/connected';
10
+ import { useDataBrowserConfig } from './DataBrowser';
11
+ import { useResource } from '@ldo/solid-react';
12
+ import { Platform } from 'react-native';
13
+
14
+ interface UseTargetResourceContext {
15
+ targetUri?: string;
16
+ targetResource?: SolidLeaf | SolidContainer | InvalidIdentifierResource;
17
+ refresh: () => void;
18
+ navigateTo: (uri: string) => void;
19
+ }
20
+
21
+ // @ts-ignore The default value will be filled in upon mount
22
+ const TargetResourceContext = createContext<UseTargetResourceContext>({});
23
+
24
+ export function useTargetResource() {
25
+ return useContext(TargetResourceContext);
26
+ }
27
+
28
+ export const TargetResourceProvider: FunctionComponent<PropsWithChildren> = ({
29
+ children,
30
+ }) => {
31
+ const { mode, origin } = useDataBrowserConfig();
32
+
33
+ /**
34
+ * URL Management
35
+ */
36
+ const [currentUrl, setCurrentUrl] = useState<URL>(() => {
37
+ return Platform.OS === 'web' ? new URL(window.location.href) : new URL('');
38
+ });
39
+
40
+ // Sync with addressbar
41
+ const handleUrlChange = useCallback(() => {
42
+ setCurrentUrl(new URL(window.location.href));
43
+ }, []);
44
+ useEffect(() => {
45
+ if (Platform.OS === 'web') {
46
+ // 1. Listen for 'popstate' events (triggered by browser back/forward buttons, or history.go())
47
+ window.addEventListener('popstate', handleUrlChange);
48
+
49
+ // 2. Patch history methods to also trigger our handler
50
+ // This ensures our hook reacts when the app itself changes the URL programmatically.
51
+ const originalPushState = history.pushState;
52
+ const originalReplaceState = history.replaceState;
53
+
54
+ history.pushState = function (...args) {
55
+ originalPushState.apply(history, args);
56
+ handleUrlChange(); // Trigger update after pushState
57
+ };
58
+
59
+ history.replaceState = function (...args) {
60
+ originalReplaceState.apply(history, args);
61
+ handleUrlChange(); // Trigger update after replaceState
62
+ };
63
+
64
+ // Cleanup: Remove event listener and restore original history methods on unmount
65
+ return () => {
66
+ window.removeEventListener('popstate', handleUrlChange);
67
+ history.pushState = originalPushState;
68
+ history.replaceState = originalReplaceState;
69
+ };
70
+ }
71
+ }, [handleUrlChange]);
72
+
73
+ const navigateTo = useCallback(
74
+ (newRoute: string) => {
75
+ const newUrl = new URL(newRoute);
76
+ let finalUrl: string;
77
+ if (mode === 'server-ui' && newUrl.origin === origin) {
78
+ finalUrl = newRoute;
79
+ } else {
80
+ finalUrl = `${origin}?uri=${encodeURIComponent(newRoute)}`;
81
+ }
82
+ setCurrentUrl(new URL(finalUrl));
83
+ if (Platform.OS === 'web') {
84
+ window.history.pushState(null, '', finalUrl);
85
+ }
86
+ },
87
+ [origin, mode],
88
+ );
89
+
90
+ /**
91
+ * Target Resource Management
92
+ */
93
+ const targetUri = useMemo<string | undefined>(() => {
94
+ const searchParams = currentUrl.searchParams.get('uri');
95
+ if (searchParams) return searchParams;
96
+ // If we're a standalone app and the uri isn't provided in the search params, it's undefined
97
+ if (mode === 'standalone-app') return undefined;
98
+ // Must be in web if this is server-hosted
99
+ const curOrigin = currentUrl.origin;
100
+ const curPathname = currentUrl.pathname;
101
+ const curHash = currentUrl.hash;
102
+ return `${curOrigin}${curPathname}${curHash}`;
103
+ }, [currentUrl, mode]);
104
+
105
+ const targetResource = useResource(targetUri);
106
+
107
+ const refresh = useCallback(async () => {
108
+ if (targetResource) {
109
+ await targetResource?.read();
110
+ }
111
+ }, [targetResource]);
112
+
113
+ const context = useMemo(
114
+ () => ({
115
+ targetUri,
116
+ targetResource,
117
+ refresh,
118
+ navigateTo,
119
+ }),
120
+ [targetUri, targetResource, refresh, navigateTo],
121
+ );
122
+
123
+ return (
124
+ <TargetResourceContext.Provider value={context}>
125
+ {children}
126
+ </TargetResourceContext.Provider>
127
+ );
128
+ };
@@ -0,0 +1,123 @@
1
+ import React, { useEffect } from 'react';
2
+ import '~/global.css';
3
+ import AsyncStorage from '@react-native-async-storage/async-storage';
4
+ import {
5
+ FunctionComponent,
6
+ PropsWithChildren,
7
+ createContext,
8
+ useContext,
9
+ useMemo,
10
+ useState,
11
+ } from 'react';
12
+ import { Appearance, ColorSchemeName, Platform } from 'react-native';
13
+ import {
14
+ ThemeProvider as ApplicationThemeProvider,
15
+ DarkTheme,
16
+ DefaultTheme,
17
+ Theme,
18
+ } from '@react-navigation/native';
19
+ import { NAV_THEME } from '~/lib/constants';
20
+ import { useColorScheme } from 'nativewind';
21
+ import { setAndroidNavigationBar } from '~/lib/android-navigation-bar';
22
+
23
+ const COLOR_SCHEME_KEY = 'colorScheme';
24
+
25
+ const LIGHT_THEME: Theme = {
26
+ ...DefaultTheme,
27
+ colors: NAV_THEME.light,
28
+ };
29
+
30
+ const DARK_THEME: Theme = {
31
+ ...DarkTheme,
32
+ colors: NAV_THEME.dark,
33
+ };
34
+
35
+ interface UseThemeChangeContext {
36
+ setColorScheme: (scheme: NonNullable<ColorSchemeName>) => void;
37
+ loadingColorScheme: boolean;
38
+ colorScheme: NonNullable<ColorSchemeName>;
39
+ }
40
+
41
+ const ThemeProviderContext = createContext<UseThemeChangeContext>({
42
+ loadingColorScheme: true,
43
+ setColorScheme: () => {},
44
+ colorScheme: 'light',
45
+ });
46
+
47
+ export function useThemeChange() {
48
+ return useContext(ThemeProviderContext);
49
+ }
50
+
51
+ const usePlatformSpecificSetup = Platform.select({
52
+ web: useSetWebBackgroundClassName,
53
+ android: useSetAndroidNavigationBar,
54
+ default: noop,
55
+ });
56
+
57
+ export const ThemeProvider: FunctionComponent<PropsWithChildren> = ({
58
+ children,
59
+ }) => {
60
+ const { colorScheme, setColorScheme } = useColorScheme();
61
+ const [loadingColorScheme, setLoadingColorScheme] = useState(true);
62
+
63
+ usePlatformSpecificSetup();
64
+
65
+ useEffect(() => {
66
+ const lookupCurColorScheme = async () => {
67
+ setColorScheme(Appearance.getColorScheme() ?? 'system');
68
+ const storedColorSchemeName: ColorSchemeName =
69
+ ((await AsyncStorage.getItem(COLOR_SCHEME_KEY)) as ColorSchemeName) ||
70
+ Appearance.getColorScheme();
71
+ if (storedColorSchemeName) {
72
+ setColorScheme(storedColorSchemeName);
73
+ }
74
+ setLoadingColorScheme(false);
75
+ };
76
+ lookupCurColorScheme();
77
+ // eslint-disable-next-line react-hooks/exhaustive-deps
78
+ }, []);
79
+
80
+ const context = useMemo(
81
+ () => ({
82
+ setColorScheme: async (newColorScheme: NonNullable<ColorSchemeName>) => {
83
+ setColorScheme(newColorScheme);
84
+ await AsyncStorage.setItem(COLOR_SCHEME_KEY, newColorScheme);
85
+ },
86
+ loadingColorScheme,
87
+ colorScheme: colorScheme ?? 'light',
88
+ }),
89
+ [loadingColorScheme, colorScheme, setColorScheme],
90
+ );
91
+
92
+ return (
93
+ <>
94
+ <ApplicationThemeProvider
95
+ value={context.colorScheme === 'light' ? LIGHT_THEME : DARK_THEME}
96
+ >
97
+ <ThemeProviderContext.Provider value={context}>
98
+ {children}
99
+ </ThemeProviderContext.Provider>
100
+ </ApplicationThemeProvider>
101
+ </>
102
+ );
103
+ };
104
+
105
+ const useIsomorphicLayoutEffect =
106
+ Platform.OS === 'web' && typeof window === 'undefined'
107
+ ? React.useEffect
108
+ : React.useLayoutEffect;
109
+
110
+ function useSetWebBackgroundClassName() {
111
+ useIsomorphicLayoutEffect(() => {
112
+ // Adds the background color to the html element to prevent white background on overscroll.
113
+ document.documentElement.classList.add('bg-background');
114
+ }, []);
115
+ }
116
+
117
+ function useSetAndroidNavigationBar() {
118
+ React.useLayoutEffect(() => {
119
+ setAndroidNavigationBar(Appearance.getColorScheme() ?? 'light');
120
+ }, []);
121
+ }
122
+
123
+ function noop() {}
@@ -0,0 +1,140 @@
1
+ // components/DialogProvider.tsx
2
+ import React, {
3
+ createContext,
4
+ useContext,
5
+ useState,
6
+ useRef,
7
+ useCallback,
8
+ } from 'react';
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogHeader,
13
+ DialogFooter,
14
+ DialogTitle,
15
+ DialogDescription,
16
+ DialogClose,
17
+ } from '~/components/ui/dialog';
18
+ import { TextInput, View } from 'react-native';
19
+ import { Button } from '~/components/ui/button';
20
+ import { Text } from '../ui/text';
21
+
22
+ type DialogOptions =
23
+ | { type: 'confirm'; title: string; message?: string }
24
+ | { type: 'prompt'; title: string; message?: string; defaultValue?: string };
25
+
26
+ type DialogContextType = {
27
+ prompt: (
28
+ title: string,
29
+ message?: string,
30
+ defaultValue?: string,
31
+ ) => Promise<null | string>;
32
+ confirm: (title: string, message?: string) => Promise<boolean>;
33
+ showDialog: (options: DialogOptions) => Promise<boolean | string>;
34
+ };
35
+
36
+ const DialogContext = createContext<DialogContextType | null>(null);
37
+
38
+ export const useDialog = () => {
39
+ const ctx = useContext(DialogContext);
40
+ if (!ctx) throw new Error('useDialog must be used within DialogProvider');
41
+ return ctx;
42
+ };
43
+
44
+ export const DialogProvider: React.FC<{ children: React.ReactNode }> = ({
45
+ children,
46
+ }) => {
47
+ const [visible, setVisible] = useState(false);
48
+ const [options, setOptions] = useState<DialogOptions | null>(null);
49
+ const [inputValue, setInputValue] = useState('');
50
+ const resolver = useRef<(val: boolean | string) => void>(() => {});
51
+
52
+ const showDialog = useCallback(
53
+ (opts: DialogOptions): Promise<boolean | string> => {
54
+ return new Promise((resolve) => {
55
+ resolver.current = resolve;
56
+ setOptions(opts);
57
+ setInputValue(opts.type === 'prompt' ? (opts.defaultValue ?? '') : '');
58
+ setVisible(true);
59
+ });
60
+ },
61
+ [],
62
+ );
63
+
64
+ const prompt = useCallback(
65
+ async (
66
+ title: string,
67
+ message?: string,
68
+ defaultValue?: string,
69
+ ): Promise<null | string> => {
70
+ return showDialog({
71
+ type: 'prompt',
72
+ title,
73
+ message,
74
+ defaultValue,
75
+ }) as Promise<string | null>;
76
+ },
77
+ [showDialog],
78
+ );
79
+
80
+ const confirm = useCallback(
81
+ async (title: string, message?: string): Promise<boolean> => {
82
+ return showDialog({
83
+ type: 'confirm',
84
+ title,
85
+ message,
86
+ }) as Promise<boolean>;
87
+ },
88
+ [showDialog],
89
+ );
90
+
91
+ const handleCancel = () => {
92
+ setVisible(false);
93
+ resolver.current?.(options?.type === 'confirm' ? false : '');
94
+ };
95
+
96
+ const handleConfirm = () => {
97
+ setVisible(false);
98
+ resolver.current?.(options?.type === 'confirm' ? true : inputValue);
99
+ };
100
+
101
+ return (
102
+ <DialogContext.Provider value={{ showDialog, prompt, confirm }}>
103
+ {children}
104
+ <Dialog open={visible} onOpenChange={setVisible}>
105
+ <DialogContent>
106
+ <DialogHeader>
107
+ <DialogTitle>{options?.title}</DialogTitle>
108
+ {options?.message && (
109
+ <DialogDescription>{options.message}</DialogDescription>
110
+ )}
111
+ </DialogHeader>
112
+
113
+ {options?.type === 'prompt' && (
114
+ <View className="my-2">
115
+ <TextInput
116
+ value={inputValue}
117
+ onChangeText={setInputValue}
118
+ placeholder="Enter text..."
119
+ className="border p-2 rounded"
120
+ autoFocus
121
+ onSubmitEditing={handleConfirm}
122
+ />
123
+ </View>
124
+ )}
125
+
126
+ <DialogFooter>
127
+ <DialogClose asChild>
128
+ <Button variant="ghost" onPress={handleCancel}>
129
+ <Text>Cancel</Text>
130
+ </Button>
131
+ </DialogClose>
132
+ <Button onPress={handleConfirm}>
133
+ <Text>Ok</Text>
134
+ </Button>
135
+ </DialogFooter>
136
+ </DialogContent>
137
+ </Dialog>
138
+ </DialogContext.Provider>
139
+ );
140
+ };
@@ -0,0 +1,118 @@
1
+ import React, {
2
+ createContext,
3
+ Dispatch,
4
+ FunctionComponent,
5
+ SetStateAction,
6
+ } from 'react';
7
+ import { useTargetResource } from '../TargetResourceProvider';
8
+ import { ResourceViewConfig } from '../ResourceView';
9
+ import { ErrorMessageResourceView } from './utilityResourceViews/ErrorMessageResourceView';
10
+ import { TextCursorInput } from '~/lib/icons/TextCursorInput';
11
+ import { CircleSlash } from '~/lib/icons/CircleSlash';
12
+ import { OctagonX } from '~/lib/icons/OctagonX';
13
+ import { ShieldX } from '~/lib/icons/ShieldX';
14
+ import { CircleX } from '~/lib/icons/CircleX';
15
+ import { Header } from './header/Header';
16
+ import { View } from 'react-native';
17
+ import { useValidView, ValidViewProvider } from './useValidView';
18
+ import { DialogProvider } from './DialogProvider';
19
+
20
+ export const ValidViewContext = createContext<{
21
+ validViews: ResourceViewConfig[];
22
+ curViewConfig: ResourceViewConfig;
23
+ setCurViewConfig: Dispatch<SetStateAction<ResourceViewConfig>>;
24
+ // @ts-ignore This will be filled in later
25
+ }>({});
26
+
27
+ export const Layout: FunctionComponent = () => {
28
+ return (
29
+ <DialogProvider>
30
+ <ValidViewProvider>
31
+ <Header />
32
+ <View className="flex-1 z-0">
33
+ <RenderView />
34
+ </View>
35
+ </ValidViewProvider>
36
+ </DialogProvider>
37
+ );
38
+ };
39
+
40
+ /**
41
+ * =============================================================================
42
+ * Render View
43
+ * =============================================================================
44
+ */
45
+
46
+ export const RenderView: FunctionComponent = () => {
47
+ const { targetUri, targetResource } = useTargetResource();
48
+
49
+ const { curViewConfig } = useValidView();
50
+
51
+ // Handle Edge cases
52
+ if (!targetResource || !targetUri) {
53
+ return (
54
+ <ErrorMessageResourceView
55
+ icon={TextCursorInput}
56
+ message="Enter a URI in the address bar to view a resource."
57
+ />
58
+ );
59
+ } else if (targetResource.type === 'InvalidIdentifierResouce') {
60
+ return (
61
+ <ErrorMessageResourceView
62
+ icon={CircleSlash}
63
+ message={`${targetResource.uri} is an invalid URI.`}
64
+ />
65
+ );
66
+ } else if (targetResource?.isDoingInitialFetch()) {
67
+ return <></>;
68
+ } else if (targetResource?.isAbsent()) {
69
+ return (
70
+ <ErrorMessageResourceView
71
+ icon={CircleSlash}
72
+ message={`${targetResource.uri} either doesn't exist or you don't have read access to it.`}
73
+ />
74
+ );
75
+ } else if (targetResource?.status.isError) {
76
+ switch (targetResource.status.type) {
77
+ case 'noncompliantPodError':
78
+ return (
79
+ <ErrorMessageResourceView
80
+ icon={OctagonX}
81
+ message={`${targetResource.uri} returned a response that is not compliant with the Linked Web Storage specification: ${targetResource.status.message}`}
82
+ />
83
+ );
84
+ case 'serverError':
85
+ return (
86
+ <ErrorMessageResourceView
87
+ icon={OctagonX}
88
+ message={`${targetResource.uri} encountered an internal server error: ${targetResource.status.message}`}
89
+ />
90
+ );
91
+ case 'unauthenticatedError':
92
+ return (
93
+ <ErrorMessageResourceView
94
+ icon={ShieldX}
95
+ message={`${targetResource.uri} requires you to log in to view.`}
96
+ />
97
+ );
98
+ case 'unauthorizedError':
99
+ return (
100
+ <ErrorMessageResourceView
101
+ icon={ShieldX}
102
+ message={`You don't have access to ${targetResource.uri}.`}
103
+ />
104
+ );
105
+ case 'unexpectedHttpError':
106
+ case 'unexpectedResourceError':
107
+ default:
108
+ return (
109
+ <ErrorMessageResourceView
110
+ icon={CircleX}
111
+ message={`An unexpected error occurred: ${targetResource.status.message}.`}
112
+ />
113
+ );
114
+ }
115
+ }
116
+ const CurView = curViewConfig.view;
117
+ return <CurView />;
118
+ };
@@ -0,0 +1,126 @@
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { FunctionComponent, useState } from 'react';
3
+ import { TouchableOpacity, View } from 'react-native';
4
+ import { Input } from '~/components/ui/input';
5
+ import { ChevronRight } from '~/lib/icons/ChevronRight';
6
+ import { ChevronsRight } from '~/lib/icons/ChevronsRight';
7
+ import { TextCursorInput } from '~/lib/icons/TextCursorInput';
8
+ import { RefreshCw } from '~/lib/icons/RefreshCw';
9
+ import { ArrowRight } from '~/lib/icons/ArrowRight';
10
+ import { Button } from '~/components/ui/button';
11
+ import { Text } from '../../ui/text';
12
+ import { useTargetResource } from '../../TargetResourceProvider';
13
+
14
+ export const AddressBox: FunctionComponent = () => {
15
+ const [isTextMode, setIsTextMode] = useState(false);
16
+ const { targetUri, refresh, navigateTo, targetResource } =
17
+ useTargetResource();
18
+
19
+ const [textBoxValue, setTextBoxValue] = useState(targetUri ?? '');
20
+ useEffect(() => {
21
+ setTextBoxValue(targetUri ?? '');
22
+ }, [targetUri]);
23
+
24
+ const breadcrumbInfo = useMemo<{ name: string; uri: string }[]>(() => {
25
+ if (!targetUri) return [];
26
+ try {
27
+ const uri = new URL(targetUri);
28
+ const pathSplit = uri.pathname.split('/').filter((val) => val !== '');
29
+ const endsInSlash = uri.pathname.endsWith('/');
30
+
31
+ let curUri = `${uri.origin}/`;
32
+ const info: { name: string; uri: string }[] = [
33
+ {
34
+ name: uri.host,
35
+ uri: curUri,
36
+ },
37
+ ];
38
+ pathSplit.forEach((name, index) => {
39
+ curUri +=
40
+ index === pathSplit.length - 1 && !endsInSlash ? name : `${name}/`;
41
+ info.push({
42
+ uri: curUri,
43
+ name,
44
+ });
45
+ });
46
+
47
+ return info;
48
+ } catch {
49
+ return [];
50
+ }
51
+ }, [targetUri]);
52
+
53
+ return (
54
+ <View className="flex-1">
55
+ <Input
56
+ className="flex-1 bg-secondary web:py-2.5 border-none pl-10 pr-10 h-[40px] text-sm web:focus-visible:ring-0 web:focus-visible:ring-transparent web:focus-visible:ring-offset-0 web:focus:outline-none web:outline-none"
57
+ onFocus={() => setIsTextMode(true)}
58
+ onBlur={() => setTimeout(() => setIsTextMode(false), 100)}
59
+ onChangeText={setTextBoxValue}
60
+ value={isTextMode ? textBoxValue : ''}
61
+ onSubmitEditing={() => {
62
+ if (textBoxValue) {
63
+ navigateTo(textBoxValue);
64
+ }
65
+ }}
66
+ />
67
+ <Button
68
+ variant="secondary"
69
+ className="absolute left-0 w-10 h-10"
70
+ onPress={() => setIsTextMode((val) => !val)}
71
+ >
72
+ <Text>
73
+ {isTextMode ? (
74
+ <ChevronsRight size={20} />
75
+ ) : (
76
+ <TextCursorInput size={20} />
77
+ )}
78
+ </Text>
79
+ </Button>
80
+ {(() => {
81
+ const shouldRefresh = targetUri === textBoxValue || !isTextMode;
82
+ return (
83
+ <Button
84
+ variant="secondary"
85
+ className="absolute right-0 w-10 h-10"
86
+ onPressIn={() => {
87
+ if (shouldRefresh) {
88
+ refresh();
89
+ } else if (textBoxValue) {
90
+ navigateTo(textBoxValue);
91
+ }
92
+ }}
93
+ >
94
+ <Text className={targetResource?.isLoading() ? 'animate-spin' : ''}>
95
+ {shouldRefresh || targetResource?.isLoading() ? (
96
+ <RefreshCw size={20} />
97
+ ) : (
98
+ <ArrowRight size={20} />
99
+ )}
100
+ </Text>
101
+ </Button>
102
+ );
103
+ })()}
104
+ <View
105
+ className="absolute top-0 left-0 right-0 bottom-0 flex-row-reverse items-center ml-10 mr-10 overflow-x-auto scrollbar-hide [direction:rtl]"
106
+ pointerEvents="none"
107
+ >
108
+ {!isTextMode &&
109
+ breadcrumbInfo.map((item, index) => (
110
+ <View className="flex-row" key={item.uri}>
111
+ {index !== breadcrumbInfo.length - 1 ? (
112
+ <ChevronRight className="w-4 h-4 mr-0.5 mt-0.5 text-gray-500" />
113
+ ) : (
114
+ <View className="w-2" />
115
+ )}
116
+ <TouchableOpacity onPress={() => navigateTo(item.uri)}>
117
+ <View pointerEvents="auto">
118
+ <Text className="mr-0.5 underline text-sm">{item.name}</Text>
119
+ </View>
120
+ </TouchableOpacity>
121
+ </View>
122
+ ))}
123
+ </View>
124
+ </View>
125
+ );
126
+ };
@@ -0,0 +1,62 @@
1
+ import React from 'react';
2
+
3
+ import { FunctionComponent } from 'react';
4
+ import { StyleSheet } from 'react-native';
5
+
6
+ export const AvatarMenu: FunctionComponent = () => {
7
+ return <></>;
8
+ // const [menuVisible, setMenuVisible] = useState(false);
9
+ // const { session, logout } = useSolidAuth();
10
+ // // TODO: Use WebId Resource to render a skeleton loader
11
+ // const webIdResource = useResource(session.webId);
12
+ // const profile = useSubject(SolidProfileShapeShapeType, session.webId);
13
+ // const renderAvatar = () => (
14
+ // <TouchableWithoutFeedback onPress={() => setMenuVisible(true)}>
15
+ // <Avatar
16
+ // source={{ uri: 'https://api.lorem.space/image/face?w=150&h=150' }}
17
+ // />
18
+ // </TouchableWithoutFeedback>
19
+ // );
20
+ // return (
21
+ // <Popover
22
+ // anchor={renderAvatar}
23
+ // visible={menuVisible}
24
+ // placement="bottom end"
25
+ // onBackdropPress={() => setMenuVisible(false)}
26
+ // style={styles.popover}
27
+ // >
28
+ // <Layout>
29
+ // <View style={styles.profileHeader}>
30
+ // <Avatar
31
+ // size="giant"
32
+ // source={{ uri: 'https://api.lorem.space/image/face?w=150&h=150' }}
33
+ // />
34
+ // <View style={styles.profileText}>
35
+ // <Text category="h6">{profile?.fn || ''}</Text>
36
+ // <Button size="tiny">Edit your profile</Button>
37
+ // </View>
38
+ // </View>
39
+ // <Divider />
40
+ // <ThemeToggleMenu />
41
+ // <Divider />
42
+ // <MenuItem
43
+ // onPress={logout}
44
+ // title="Log Out"
45
+ // accessoryLeft={(props) => <Icon {...props} name="log-out" />}
46
+ // />
47
+ // </Layout>
48
+ // </Popover>
49
+ // );
50
+ };
51
+
52
+ const styles = StyleSheet.create({
53
+ popover: { width: 300, overflow: 'hidden', borderRadius: 12 },
54
+ profileHeader: { padding: 8, flexDirection: 'row', alignItems: 'center' },
55
+ profileText: {
56
+ marginLeft: 8,
57
+ justifyContent: 'space-around',
58
+ alignSelf: 'stretch',
59
+ alignItems: 'flex-start',
60
+ flex: 1,
61
+ },
62
+ });