diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53178a699..d9b04cd13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,6 +61,8 @@ If you are having issues ensure you are using the following versions of Rust and - Rust version: **1.67.0** - Node version: **18** +Be sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to make sure your code is a similar style to ours. + ##### Mobile app To run mobile app diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 92c63dad1..58e1bb966 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -6,8 +6,14 @@ import { listen } from '@tauri-apps/api/event'; import { convertFileSrc } from '@tauri-apps/api/tauri'; import { useEffect } from 'react'; import { getDebugState, hooks } from '@sd/client'; -import SpacedriveInterface, { OperatingSystem, Platform, PlatformProvider } from '@sd/interface'; -import { KeybindEvent, ErrorPage } from '@sd/interface'; +import { + ErrorPage, + KeybindEvent, + OperatingSystem, + Platform, + PlatformProvider, + SpacedriveInterface +} from '@sd/interface'; import '@sd/ui/style'; const client = hooks.createClient({ diff --git a/apps/desktop/src/index.tsx b/apps/desktop/src/index.tsx index e7337a723..1ef413b33 100644 --- a/apps/desktop/src/index.tsx +++ b/apps/desktop/src/index.tsx @@ -1,7 +1,6 @@ // WARNING: BE CAREFUL SAVING THIS FILE WITH A FORMATTER ENABLED. The import order is important and goes against prettier's recommendations. import React, { Suspense } from 'react'; import ReactDOM from 'react-dom/client'; -import '@sd/ui/style'; // THIS MUST GO BEFORE importing the App import '~/patches'; import App from './App'; diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index fb1b3f986..d3855c6fb 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -10,7 +10,7 @@ "include": ["src"], "references": [ { - "path": "../../packages/interface" + "path": "../../interface" } ] } diff --git a/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx b/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx index 6ed18f47f..0bb2225be 100644 --- a/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx +++ b/apps/mobile/src/components/explorer/sections/FavoriteButton.tsx @@ -1,8 +1,8 @@ +import { useQueryClient } from '@tanstack/react-query'; import { Heart } from 'phosphor-react-native'; import { useState } from 'react'; import { Pressable, PressableProps } from 'react-native'; import { Object as SDObject, useLibraryMutation } from '@sd/client'; -import { useQueryClient } from '@tanstack/react-query'; type Props = { data: SDObject; diff --git a/apps/mobile/src/components/explorer/sections/Note.tsx b/apps/mobile/src/components/explorer/sections/Note.tsx index 513dbb320..fde61a377 100644 --- a/apps/mobile/src/components/explorer/sections/Note.tsx +++ b/apps/mobile/src/components/explorer/sections/Note.tsx @@ -22,7 +22,7 @@ const Note = (props: Props) => { 2000 ); - const debouncedNote = useCallback((note: string) => debounce(note), [props.data.id, fileSetNote]); + const debouncedNote = useCallback((note: string) => debounce(note), [debounce]); return ( diff --git a/apps/mobile/src/components/modal/confirm-modals/DeleteLibraryModal.tsx b/apps/mobile/src/components/modal/confirm-modals/DeleteLibraryModal.tsx index b7f04b7c8..0dba8aca1 100644 --- a/apps/mobile/src/components/modal/confirm-modals/DeleteLibraryModal.tsx +++ b/apps/mobile/src/components/modal/confirm-modals/DeleteLibraryModal.tsx @@ -1,7 +1,7 @@ import { useQueryClient } from '@tanstack/react-query'; -import { useBridgeMutation } from '@sd/client'; import { useRef } from 'react'; -import { ModalRef, ConfirmModal } from '~/components/layout/Modal'; +import { useBridgeMutation } from '@sd/client'; +import { ConfirmModal, ModalRef } from '~/components/layout/Modal'; type Props = { libraryUuid: string; diff --git a/apps/mobile/src/components/modal/tag/CreateTagModal.tsx b/apps/mobile/src/components/modal/tag/CreateTagModal.tsx index aca5ec983..f3bd34659 100644 --- a/apps/mobile/src/components/modal/tag/CreateTagModal.tsx +++ b/apps/mobile/src/components/modal/tag/CreateTagModal.tsx @@ -1,3 +1,4 @@ +import { useQueryClient } from '@tanstack/react-query'; import { forwardRef, useEffect, useState } from 'react'; import { Pressable, Text, View } from 'react-native'; import ColorPicker from 'react-native-wheel-color-picker'; @@ -8,7 +9,6 @@ import { Modal, ModalRef } from '~/components/layout/Modal'; import { Button } from '~/components/primitive/Button'; import useForwardedRef from '~/hooks/useForwardedRef'; import { tw, twStyle } from '~/lib/tailwind'; -import { useQueryClient } from '@tanstack/react-query'; const CreateTagModal = forwardRef((_, ref) => { const queryClient = useQueryClient(); diff --git a/apps/mobile/src/components/modal/tag/UpdateTagModal.tsx b/apps/mobile/src/components/modal/tag/UpdateTagModal.tsx index 703fe4967..b878141c8 100644 --- a/apps/mobile/src/components/modal/tag/UpdateTagModal.tsx +++ b/apps/mobile/src/components/modal/tag/UpdateTagModal.tsx @@ -1,3 +1,4 @@ +import { useQueryClient } from '@tanstack/react-query'; import { forwardRef, useEffect, useState } from 'react'; import { Pressable, Text, View } from 'react-native'; import { Tag, useLibraryMutation } from '@sd/client'; @@ -8,7 +9,6 @@ import { Modal, ModalRef } from '~/components/layout/Modal'; import { Button } from '~/components/primitive/Button'; import useForwardedRef from '~/hooks/useForwardedRef'; import { tw, twStyle } from '~/lib/tailwind'; -import { useQueryClient } from '@tanstack/react-query'; type Props = { tag: Tag; diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 18ac2b387..4af2729af 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -2,7 +2,7 @@ import { createWSClient, loggerLink, wsLink } from '@rspc/client'; import { QueryClient, QueryClientProvider, hydrate } from '@tanstack/react-query'; import { useEffect } from 'react'; import { getDebugState, hooks } from '@sd/client'; -import SpacedriveInterface, { Platform, PlatformProvider } from '@sd/interface'; +import { Platform, PlatformProvider, SpacedriveInterface } from '@sd/interface'; import demoData from './demoData.json'; globalThis.isDev = import.meta.env.DEV; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index e45eb13df..17fba410b 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -7,7 +7,7 @@ "include": ["src", "src/demoData.json"], "references": [ { - "path": "../../packages/interface" + "path": "../../interface" } ] } diff --git a/docs/developers/prerequisites/guidelines.md b/docs/developers/prerequisites/guidelines.md new file mode 100644 index 000000000..054f001d5 --- /dev/null +++ b/docs/developers/prerequisites/guidelines.md @@ -0,0 +1,26 @@ +--- +index: 2 +--- + +# Guidelines + +## `@sd/interface` + +Most interface code should live inside the `app` directory, +with the folder structure resembling the app's routing structure. +We currently use [React Router](https://reactrouter.com/) and take full advantage of nested and config-based routing + +### Casing + +- All files/folders containing a route should be `lower-kebab-case` +- Dynamic routes should be `camelCase` and have their parameter name prefixed with `$` +- All other files/folders should be `PascalCase` (expect for `index` files inside `PascalCase` folders) + +### Layouts + +If a folder of routes has a component that should be applied to _every_ sub-route, +the component's file should be named `Layout.tsx` and applied in the parent folder's routing configuration as the `element` property. + +For components that should wrap a subset of routes, +name the file with something ending in `Layout.tsx` (but not `Layout.tsx` itself!). +We then recommend using [layout routes](https://reactrouter.com/en/main/route/route#layout-routes) to apply the layout without introducing a new `path` segment. diff --git a/packages/interface/.eslintrc.js b/interface/.eslintrc.js similarity index 100% rename from packages/interface/.eslintrc.js rename to interface/.eslintrc.js diff --git a/packages/interface/src/ErrorFallback.tsx b/interface/ErrorFallback.tsx similarity index 82% rename from packages/interface/src/ErrorFallback.tsx rename to interface/ErrorFallback.tsx index b24a7b6a6..a6be2fad9 100644 --- a/packages/interface/src/ErrorFallback.tsx +++ b/interface/ErrorFallback.tsx @@ -3,18 +3,16 @@ import { FallbackProps } from 'react-error-boundary'; import { useDebugState } from '@sd/client'; import { Button } from '@sd/ui'; -export function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { - return ( - { - captureException(error); - resetErrorBoundary(); - }} - reloadBtn={resetErrorBoundary} - /> - ); -} +export default ({ error, resetErrorBoundary }: FallbackProps) => ( + { + captureException(error); + resetErrorBoundary(); + }} + reloadBtn={resetErrorBoundary} + /> +); export function ErrorPage({ reloadBtn, diff --git a/packages/interface/src/screens/NotFound.tsx b/interface/app/$libraryId/404.tsx similarity index 95% rename from packages/interface/src/screens/NotFound.tsx rename to interface/app/$libraryId/404.tsx index 0d2037a31..d247ffa2f 100644 --- a/packages/interface/src/screens/NotFound.tsx +++ b/interface/app/$libraryId/404.tsx @@ -1,8 +1,9 @@ import { useNavigate } from 'react-router'; import { Button } from '@sd/ui'; -export default function NotFound() { +export default () => { const navigate = useNavigate(); + return (
); -} +}; diff --git a/interface/app/$libraryId/Explorer/ContextMenu.tsx b/interface/app/$libraryId/Explorer/ContextMenu.tsx new file mode 100644 index 000000000..383df1de5 --- /dev/null +++ b/interface/app/$libraryId/Explorer/ContextMenu.tsx @@ -0,0 +1,139 @@ +import { Clipboard, FileX, Image, Plus, Repeat, Share, ShieldCheck } from 'phosphor-react'; +import { PropsWithChildren, useMemo } from 'react'; +import { useLibraryMutation } from '@sd/client'; +import { ContextMenu as CM } from '@sd/ui'; +import { useExplorerParams } from '~/app/$libraryId/location/$id'; +import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore'; +import { useOperatingSystem } from '~/hooks/useOperatingSystem'; +import { usePlatform } from '~/util/Platform'; + +export const OpenInNativeExplorer = () => { + const platform = usePlatform(); + const os = useOperatingSystem(); + + const osFileBrowserName = useMemo(() => { + if (os === 'macOS') { + return 'Finder'; + } else { + return 'Explorer'; + } + }, [os]); + + return ( + <> + {platform.openPath && ( + { + alert('TODO: Open in FS'); + // console.log('TODO', store.contextMenuActiveItem); + // platform.openPath!('/Users/oscar/Desktop'); // TODO: Work out the file path from the backend + }} + /> + )} + + ); +}; + +export default (props: PropsWithChildren) => { + const store = useExplorerStore(); + const params = useExplorerParams(); + + const generateThumbsForLocation = useLibraryMutation('jobs.generateThumbsForLocation'); + const objectValidator = useLibraryMutation('jobs.objectValidator'); + const rescanLocation = useLibraryMutation('locations.fullRescan'); + const copyFiles = useLibraryMutation('files.copyFiles'); + const cutFiles = useLibraryMutation('files.cutFiles'); + + return ( +
+ + + + + + { + e.preventDefault(); + + navigator.share?.({ + title: 'Spacedrive', + text: 'Check out this cool app', + url: 'https://spacedrive.com' + }); + }} + /> + + + + store.locationId && rescanLocation.mutate(store.locationId)} + label="Re-index" + icon={Repeat} + /> + + +
+ ); +}; diff --git a/interface/app/$libraryId/Explorer/File/ContextMenu.tsx b/interface/app/$libraryId/Explorer/File/ContextMenu.tsx new file mode 100644 index 000000000..330f423ee --- /dev/null +++ b/interface/app/$libraryId/Explorer/File/ContextMenu.tsx @@ -0,0 +1,288 @@ +import { + ArrowBendUpRight, + Copy, + FileX, + LockSimple, + LockSimpleOpen, + Package, + Plus, + Scissors, + Share, + TagSimple, + Trash, + TrashSimple +} from 'phosphor-react'; +import { PropsWithChildren } from 'react'; +import { + ExplorerItem, + isObject, + useLibraryContext, + useLibraryMutation, + useLibraryQuery +} from '@sd/client'; +import { ContextMenu, dialogManager } from '@sd/ui'; +import { useExplorerParams } from '~/app/$libraryId/location/$id'; +import { showAlertDialog } from '~/components/AlertDialog'; +import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore'; +import { usePlatform } from '~/util/Platform'; +import { OpenInNativeExplorer } from '../ContextMenu'; +import DecryptDialog from './DecryptDialog'; +import DeleteDialog from './DeleteDialog'; +import EncryptDialog from './EncryptDialog'; +import EraseDialog from './EraseDialog'; + +interface Props extends PropsWithChildren { + data: ExplorerItem; +} + +export default ({ data, ...props }: Props) => { + const { library } = useLibraryContext(); + const store = useExplorerStore(); + const params = useExplorerParams(); + const platform = usePlatform(); + const objectData = data ? (isObject(data) ? data.item : data.item.object) : null; + + const keyManagerUnlocked = useLibraryQuery(['keys.isUnlocked']).data ?? false; + const mountedKeys = useLibraryQuery(['keys.listMounted']); + const hasMountedKeys = mountedKeys.data?.length ?? 0 > 0; + + const copyFiles = useLibraryMutation('files.copyFiles'); + + return ( +
+ + { + // TODO: Replace this with a proper UI + window.location.href = platform.getFileUrl( + library.uuid, + store.locationId!, + data.item.id + ); + }} + icon={Copy} + /> + + + + + {!store.showInspector && ( + <> + (getExplorerStore().showInspector = true)} + /> + + + )} + + + + + + + + { + copyFiles.mutate({ + source_location_id: store.locationId!, + source_path_id: data.item.id, + target_location_id: store.locationId!, + target_path: params.path, + target_file_name_suffix: ' copy' + }); + }} + /> + + { + getExplorerStore().cutCopyState = { + sourceLocationId: store.locationId!, + sourcePathId: data.item.id, + actionType: 'Cut', + active: true + }; + }} + icon={Scissors} + /> + + { + getExplorerStore().cutCopyState = { + sourceLocationId: store.locationId!, + sourcePathId: data.item.id, + actionType: 'Copy', + active: true + }; + }} + icon={Copy} + /> + + +
+ ); +}; + +const AssignTagMenuItems = (props: { objectId: number }) => { + const tags = useLibraryQuery(['tags.list'], { suspense: true }); + const tagsForObject = useLibraryQuery(['tags.getForObject', props.objectId], { suspense: true }); + const assignTag = useLibraryMutation('tags.assign'); + + return ( + <> + {tags.data?.map((tag, index) => { + const active = !!tagsForObject.data?.find((t) => t.id === tag.id); + + return ( + { + e.preventDefault(); + if (props.objectId === null) return; + + assignTag.mutate({ + tag_id: tag.id, + object_id: props.objectId, + unassign: active + }); + }} + > +
+

{tag.name}

+ + ); + })} + + ); +}; diff --git a/packages/interface/src/components/dialog/DecryptFileDialog.tsx b/interface/app/$libraryId/Explorer/File/DecryptDialog.tsx similarity index 52% rename from packages/interface/src/components/dialog/DecryptFileDialog.tsx rename to interface/app/$libraryId/Explorer/File/DecryptDialog.tsx index 1b655f2d8..7802db75c 100644 --- a/packages/interface/src/components/dialog/DecryptFileDialog.tsx +++ b/interface/app/$libraryId/Explorer/File/DecryptDialog.tsx @@ -1,17 +1,11 @@ import { RadioGroup } from '@headlessui/react'; -import { Eye, EyeSlash, Info } from 'phosphor-react'; -import { useState } from 'react'; +import { Info } from 'phosphor-react'; import { useLibraryMutation, useLibraryQuery } from '@sd/client'; import { Button, Dialog, UseDialogProps, useDialog } from '@sd/ui'; -import { Input, Switch, useZodForm, z } from '@sd/ui/src/forms'; -import { showAlertDialog } from '~/util/dialog'; -import { usePlatform } from '../../util/Platform'; -import { Tooltip } from '../tooltip/Tooltip'; - -interface DecryptDialogProps extends UseDialogProps { - location_id: number; - path_id: number; -} +import { Tooltip } from '@sd/ui'; +import { PasswordInput, Switch, useZodForm, z } from '@sd/ui/src/forms'; +import { showAlertDialog } from '~/components/AlertDialog'; +import { usePlatform } from '~/util/Platform'; const schema = z.object({ type: z.union([z.literal('password'), z.literal('key')]), @@ -21,7 +15,12 @@ const schema = z.object({ saveToKeyManager: z.boolean() }); -export const DecryptFileDialog = (props: DecryptDialogProps) => { +interface Props extends UseDialogProps { + location_id: number; + path_id: number; +} + +export default (props: Props) => { const platform = usePlatform(); const dialog = useDialog(props); @@ -55,10 +54,6 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => { } }); - const [show, setShow] = useState({ password: false }); - - const PasswordCurrentEyeIcon = show.password ? EyeSlash : Eye; - const form = useZodForm({ defaultValues: { type: hasMountedKeys ? 'key' : 'password', @@ -91,13 +86,13 @@ export const DecryptFileDialog = (props: DecryptDialogProps) => { loading={decryptFile.isLoading} ctaLabel="Decrypt" > - form.setValue('type', e)} - className="mt-2" - > - Key Type -
+
+

Key Type

+ form.setValue('type', e)} + className="mt-2 flex flex-row gap-2" + > {({ checked }) => ( )} -
- + - {form.watch('type') === 'key' && ( -
-
+ {form.watch('type') === 'key' && ( +
{ checked={form.watch('mountAssociatedKey')} onCheckedChange={(e) => form.setValue('mountAssociatedKey', e)} /> + Automatically mount key + + +
- Automatically mount key - - - -
- )} + )} - {form.watch('type') === 'password' && ( - <> -
- + - -
-
-
+
+ Save to Key Manager + + +
- Save to Key Manager - - - -
- - )} + + )} -
-
- Output file - - -
+ return; + } + platform.saveFilePickerDialog().then((result) => { + if (result) form.setValue('outputPath', result as string); + }); + }} + > + Select +
); diff --git a/packages/interface/src/components/dialog/DeleteFileDialog.tsx b/interface/app/$libraryId/Explorer/File/DeleteDialog.tsx similarity index 74% rename from packages/interface/src/components/dialog/DeleteFileDialog.tsx rename to interface/app/$libraryId/Explorer/File/DeleteDialog.tsx index 0599c0b8d..a209334b0 100644 --- a/packages/interface/src/components/dialog/DeleteFileDialog.tsx +++ b/interface/app/$libraryId/Explorer/File/DeleteDialog.tsx @@ -1,20 +1,18 @@ import { useLibraryMutation } from '@sd/client'; import { Dialog, UseDialogProps, useDialog } from '@sd/ui'; -import { useZodForm, z } from '@sd/ui/src/forms'; +import { useZodForm } from '@sd/ui/src/forms'; -interface DeleteDialogProps extends UseDialogProps { +interface Propps extends UseDialogProps { location_id: number; path_id: number; } -const schema = z.object({}); - -export const DeleteFileDialog = (props: DeleteDialogProps) => { +export default (props: Propps) => { const dialog = useDialog(props); const deleteFile = useLibraryMutation('files.deleteFiles'); - const form = useZodForm({ - schema - }); + + const form = useZodForm(); + const onSubmit = form.handleSubmit(() => deleteFile.mutateAsync({ location_id: props.location_id, diff --git a/packages/interface/src/components/dialog/EncryptFileDialog.tsx b/interface/app/$libraryId/Explorer/File/EncryptDialog.tsx similarity index 86% rename from packages/interface/src/components/dialog/EncryptFileDialog.tsx rename to interface/app/$libraryId/Explorer/File/EncryptDialog.tsx index bde7df3a5..f5b06fe3e 100644 --- a/packages/interface/src/components/dialog/EncryptFileDialog.tsx +++ b/interface/app/$libraryId/Explorer/File/EncryptDialog.tsx @@ -1,12 +1,17 @@ -import { Algorithm, useLibraryMutation, useLibraryQuery } from '@sd/client'; +import { + Algorithm, + hashingAlgoSlugSchema, + slugFromHashingAlgo, + useLibraryMutation, + useLibraryQuery +} from '@sd/client'; import { Button, Dialog, Select, SelectOption, UseDialogProps, useDialog } from '@sd/ui'; import { CheckBox, useZodForm, z } from '@sd/ui/src/forms'; -import { getHashingAlgorithmString } from '~/screens/settings/library/KeysSetting'; +import { showAlertDialog } from '~/components/AlertDialog'; import { usePlatform } from '~/util/Platform'; -import { showAlertDialog } from '~/util/dialog'; -import { SelectOptionKeyList } from '../key/KeyList'; +import { KeyListSelectOptions } from '../../KeyManager/List'; -interface EncryptDialogProps extends UseDialogProps { +interface Props extends UseDialogProps { location_id: number; path_id: number; } @@ -14,13 +19,13 @@ interface EncryptDialogProps extends UseDialogProps { const schema = z.object({ key: z.string(), encryptionAlgo: z.string(), - hashingAlgo: z.string(), + hashingAlgo: hashingAlgoSlugSchema, metadata: z.boolean(), previewMedia: z.boolean(), outputPath: z.string() }); -export const EncryptFileDialog = ({ ...props }: EncryptDialogProps) => { +export default (props: Props) => { const dialog = useDialog(props); const platform = usePlatform(); @@ -29,7 +34,7 @@ export const EncryptFileDialog = ({ ...props }: EncryptDialogProps) => { const hashAlg = keys.data?.find((key) => { return key.uuid === uuid; })?.hashing_algorithm; - hashAlg && form.setValue('hashingAlgo', getHashingAlgorithmString(hashAlg)); + hashAlg && form.setValue('hashingAlgo', slugFromHashingAlgo(hashAlg)); }; const keys = useLibraryQuery(['keys.list']); @@ -44,17 +49,13 @@ export const EncryptFileDialog = ({ ...props }: EncryptDialogProps) => { showAlertDialog({ title: 'Success', value: - 'The encryption job has started successfully. You may track the progress in the job overview panel.', - inputBox: false, - description: '' + 'The encryption job has started successfully. You may track the progress in the job overview panel.' }); }, onError: () => { showAlertDialog({ title: 'Error', - value: 'The encryption job failed to start.', - inputBox: false, - description: '' + value: 'The encryption job failed to start.' }); } }); @@ -96,7 +97,7 @@ export const EncryptFileDialog = ({ ...props }: EncryptDialogProps) => { UpdateKey(e); }} > - {mountedUuids.data && } + {mountedUuids.data && }
diff --git a/packages/interface/src/components/dialog/EraseFileDialog.tsx b/interface/app/$libraryId/Explorer/File/EraseDialog.tsx similarity index 87% rename from packages/interface/src/components/dialog/EraseFileDialog.tsx rename to interface/app/$libraryId/Explorer/File/EraseDialog.tsx index 4632710a0..390c6aeb9 100644 --- a/packages/interface/src/components/dialog/EraseFileDialog.tsx +++ b/interface/app/$libraryId/Explorer/File/EraseDialog.tsx @@ -1,10 +1,9 @@ import { useState } from 'react'; import { useLibraryMutation } from '@sd/client'; -import { Dialog, UseDialogProps, useDialog } from '@sd/ui'; +import { Dialog, Slider, UseDialogProps, useDialog } from '@sd/ui'; import { useZodForm, z } from '@sd/ui/src/forms'; -import Slider from '../primitive/Slider'; -interface EraseDialogProps extends UseDialogProps { +interface Props extends UseDialogProps { location_id: number; path_id: number; } @@ -13,7 +12,7 @@ const schema = z.object({ passes: z.number() }); -export const EraseFileDialog = (props: EraseDialogProps) => { +export default (props: Props) => { const dialog = useDialog(props); const eraseFile = useLibraryMutation('files.eraseFiles'); diff --git a/packages/interface/src/components/explorer/FileItem.tsx b/interface/app/$libraryId/Explorer/File/Item.tsx similarity index 62% rename from packages/interface/src/components/explorer/FileItem.tsx rename to interface/app/$libraryId/Explorer/File/Item.tsx index 581a2567c..20dfdd6e4 100644 --- a/packages/interface/src/components/explorer/FileItem.tsx +++ b/interface/app/$libraryId/Explorer/File/Item.tsx @@ -1,23 +1,9 @@ import clsx from 'clsx'; import { HTMLAttributes } from 'react'; import { ExplorerItem, ObjectKind, isObject } from '@sd/client'; -import { cva, tw } from '@sd/ui'; import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore'; -import { ExplorerItemContextMenu } from './ExplorerContextMenu'; -import { FileThumb } from './FileThumb'; - -const NameArea = tw.div`flex justify-center`; - -const nameContainerStyles = cva( - 'cursor-default truncate rounded-md px-1.5 py-[1px] text-center text-xs font-medium', - { - variants: { - selected: { - true: 'bg-accent text-white' - } - } - } -); +import ContextMenu from './ContextMenu'; +import FileThumb from './Thumb'; interface Props extends HTMLAttributes { data: ExplorerItem; @@ -26,16 +12,14 @@ interface Props extends HTMLAttributes { } function FileItem({ data, selected, index, ...rest }: Props) { - const objectData = data ? (isObject(data) ? data.item : data.item.object) : null; - const isVid = ObjectKind[objectData?.kind || 0] === 'Video'; const item = data.item; const explorerStore = useExplorerStore(); return ( - +
{ + onContextMenu={() => { if (index != undefined) { getExplorerStore().selectedRowIndex = index; } @@ -59,14 +43,19 @@ function FileItem({ data, selected, index, ...rest }: Props) { >
- - +
+ {item.name} {item.extension && `.${item.extension}`} - +
- + ); } diff --git a/packages/interface/src/components/explorer/FileRow.tsx b/interface/app/$libraryId/Explorer/File/Row.tsx similarity index 57% rename from packages/interface/src/components/explorer/FileRow.tsx rename to interface/app/$libraryId/Explorer/File/Row.tsx index 3ff646061..643a2857d 100644 --- a/packages/interface/src/components/explorer/FileRow.tsx +++ b/interface/app/$libraryId/Explorer/File/Row.tsx @@ -3,12 +3,11 @@ import clsx from 'clsx'; import dayjs from 'dayjs'; import { HTMLAttributes } from 'react'; import { ExplorerItem, ObjectKind, isObject, isPath } from '@sd/client'; -import { getExplorerStore } from '../../hooks/useExplorerStore'; -import { ExplorerItemContextMenu } from './ExplorerContextMenu'; -import { ColumnKey, columns } from './FileColumns'; -import { FileThumb } from './FileThumb'; -import { InfoPill } from './Inspector'; -import { getExplorerItemData } from './util'; +import { InfoPill } from '../Inspector'; +import { getExplorerItemData } from '../util'; +import ContextMenu from './ContextMenu'; +import { columns } from './RowHeader'; +import FileThumb from './Thumb'; interface Props extends HTMLAttributes { data: ExplorerItem; @@ -16,35 +15,35 @@ interface Props extends HTMLAttributes { selected: boolean; } -function FileRow({ data, index, selected, ...props }: Props) { - return ( - -
- {columns.map((col) => ( -
- -
- ))} -
-
- ); +export default ({ data, index, selected, ...props }: Props) => ( + +
+ {columns.map((col) => ( +
+ +
+ ))} +
+
+); + +interface CellProps { + colKey: (typeof columns)[number]['key']; + data: ExplorerItem; } -const RenderCell: React.FC<{ - colKey: ColumnKey; - data: ExplorerItem; -}> = ({ colKey, data }) => { +const Cell = ({ colKey, data }: CellProps) => { const objectData = data ? (isObject(data) ? data.item : data.item.object) : null; const { cas_id } = getExplorerItemData(data); @@ -83,14 +82,8 @@ const RenderCell: React.FC<{
); - // case 'meta_integrity_hash': - // return {value}; - // case 'tags': - // return renderCellWithIcon(MusicNoteIcon); default: return <>; } }; - -export default FileRow; diff --git a/interface/app/$libraryId/Explorer/File/RowHeader.tsx b/interface/app/$libraryId/Explorer/File/RowHeader.tsx new file mode 100644 index 000000000..96b2064aa --- /dev/null +++ b/interface/app/$libraryId/Explorer/File/RowHeader.tsx @@ -0,0 +1,32 @@ +interface Column { + column: string; + key: string; + width: number; +} + +export const columns = [ + { column: 'Name', key: 'name', width: 280 }, + { column: 'Type', key: 'extension', width: 150 }, + { column: 'Size', key: 'size', width: 100 }, + { column: 'Date Created', key: 'date_created', width: 150 }, + { column: 'Content ID', key: 'cas_id', width: 150 } +] as const satisfies Readonly; + +export const ROW_HEADER_HEIGHT = 40; + +export const RowHeader = () => ( +
+ {columns.map((col) => ( +
+ {col.column} +
+ ))} +
+); diff --git a/packages/interface/src/components/explorer/FileThumb.tsx b/interface/app/$libraryId/Explorer/File/Thumb.tsx similarity index 89% rename from packages/interface/src/components/explorer/FileThumb.tsx rename to interface/app/$libraryId/Explorer/File/Thumb.tsx index 501b03ac1..8a721e4db 100644 --- a/packages/interface/src/components/explorer/FileThumb.tsx +++ b/interface/app/$libraryId/Explorer/File/Thumb.tsx @@ -8,18 +8,18 @@ import Video from '@sd/assets/images/Video.png'; import clsx from 'clsx'; import { CSSProperties } from 'react'; import { ExplorerItem } from '@sd/client'; +import { Folder } from '@sd/ui'; import { usePlatform } from '~/util/Platform'; -import { Folder } from '../icons/Folder'; -import { getExplorerItemData } from './util'; +import { getExplorerItemData } from '../util'; // const icons = import.meta.glob('../../../../assets/icons/*.svg'); -interface FileItemProps { +interface Props { data: ExplorerItem; size: number; className?: string; } -export function FileThumb({ data, size, className }: FileItemProps) { +export default ({ data, size, className }: Props) => { const { cas_id, isDir, kind, hasThumbnail, extension } = getExplorerItemData(data); // 10 percent of the size @@ -59,14 +59,14 @@ export function FileThumb({ data, size, className }: FileItemProps) { : {} } /> - {extension && kind === 'Video' && size > 80 && ( -
+ {extension && kind === 'Video' && hasThumbnail && size > 80 && ( +
{extension}
)}
); -} +}; interface FileThumbImgProps { isDir: boolean; cas_id: string | null; diff --git a/packages/interface/src/components/explorer/inspector/FavoriteButton.tsx b/interface/app/$libraryId/Explorer/Inspector/FavoriteButton.tsx similarity index 100% rename from packages/interface/src/components/explorer/inspector/FavoriteButton.tsx rename to interface/app/$libraryId/Explorer/Inspector/FavoriteButton.tsx diff --git a/packages/interface/src/components/explorer/inspector/Note.tsx b/interface/app/$libraryId/Explorer/Inspector/Note.tsx similarity index 88% rename from packages/interface/src/components/explorer/inspector/Note.tsx rename to interface/app/$libraryId/Explorer/Inspector/Note.tsx index aa24f9dac..dce412ac6 100644 --- a/packages/interface/src/components/explorer/inspector/Note.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/Note.tsx @@ -1,10 +1,8 @@ import { useCallback, useState } from 'react'; import { useDebouncedCallback } from 'use-debounce'; -import { useLibraryMutation } from '@sd/client'; -import { Object as SDObject } from '@sd/client'; -import { TextArea } from '@sd/ui'; +import { Object as SDObject, useLibraryMutation } from '@sd/client'; +import { Divider, TextArea } from '@sd/ui'; import { MetaContainer, MetaTitle } from '../Inspector'; -import { Divider } from './Divider'; interface Props { data: SDObject; diff --git a/packages/interface/src/components/explorer/Inspector.tsx b/interface/app/$libraryId/Explorer/Inspector/index.tsx similarity index 90% rename from packages/interface/src/components/explorer/Inspector.tsx rename to interface/app/$libraryId/Explorer/Inspector/index.tsx index ff2d36dda..ac28762d0 100644 --- a/packages/interface/src/components/explorer/Inspector.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/index.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import dayjs from 'dayjs'; import { Barcode, CircleWavyCheck, Clock, Cube, Hash, Link, Lock, Snowflake } from 'phosphor-react'; -import { useEffect, useState } from 'react'; +import { ComponentProps, useEffect, useState } from 'react'; import { ExplorerContext, ExplorerItem, @@ -11,24 +11,17 @@ import { isObject, useLibraryQuery } from '@sd/client'; -import { Button, tw } from '@sd/ui'; -import { DefaultProps } from '../primitive/types'; -import { Tooltip } from '../tooltip/Tooltip'; -import { FileThumb } from './FileThumb'; -import { Divider } from './inspector/Divider'; -import FavoriteButton from './inspector/FavoriteButton'; -import Note from './inspector/Note'; +import { Button, Divider, Tooltip, tw } from '@sd/ui'; +import FileThumb from '../File/Thumb'; +import FavoriteButton from './FavoriteButton'; +import Note from './Note'; export const InfoPill = tw.span`inline border border-transparent px-1 text-[11px] font-medium shadow shadow-app-shade/5 bg-app-selected rounded-md text-ink-dull`; - -export const PlaceholderPill = tw.span`inline border px-1 text-[11px] shadow shadow-app-shade/10 rounded-md bg-transparent border-dashed border-app-active transition hover:text-ink-faint hover:border-ink-faint font-medium text-ink-faint/70`; +export const PlaceholderPill = tw.span`inline border px-1 text-[11px] shadow shadow-app-shade/10 rounded-md bg-transparent border-dashed border-app-active transition hover:text-ink-faint hover:border-ink-faint font-medium text-ink-faint/70`; export const MetaContainer = tw.div`flex flex-col px-4 py-1.5`; - export const MetaTitle = tw.h5`text-xs font-bold`; - export const MetaKeyName = tw.h5`text-xs flex-shrink-0 flex-wrap-0`; - export const MetaValue = tw.p`text-xs break-all text-ink truncate`; const MetaTextLine = tw.div`flex items-center my-0.5 text-xs text-ink-dull`; @@ -37,7 +30,7 @@ const InspectorIcon = ({ component: Icon, ...props }: any) => ( ); -interface Props extends DefaultProps { +interface Props extends ComponentProps<'div'> { context?: ExplorerContext; data?: ExplorerItem; } diff --git a/packages/interface/src/components/explorer/ExplorerOptionsPanel.tsx b/interface/app/$libraryId/Explorer/OptionsPanel.tsx similarity index 67% rename from packages/interface/src/components/explorer/ExplorerOptionsPanel.tsx rename to interface/app/$libraryId/Explorer/OptionsPanel.tsx index ffa8005c9..6abeb78bf 100644 --- a/packages/interface/src/components/explorer/ExplorerOptionsPanel.tsx +++ b/interface/app/$libraryId/Explorer/OptionsPanel.tsx @@ -1,15 +1,9 @@ -import { PropsWithChildren, useState } from 'react'; -import { Select, SelectOption } from '@sd/ui'; -import { getExplorerStore, useExplorerStore } from '../../hooks/useExplorerStore'; -import Slider from '../primitive/Slider'; +import { useState } from 'react'; +import { Select, SelectOption, Slider, tw } from '@sd/ui'; +import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore'; -function Heading({ children }: PropsWithChildren) { - return
{children}
; -} - -function SubHeading({ children }: PropsWithChildren) { - return
{children}
; -} +const Heading = tw.div`text-ink-dull text-xs font-semibold`; +const Subheading = tw.div`text-ink-dull mb-1 text-xs font-medium`; const sortOptions = { name: 'Name', @@ -20,7 +14,7 @@ const sortOptions = { date_last_opened: 'Date Last Opened' }; -export function ExplorerOptionsPanel() { +export default () => { const [sortBy, setSortBy] = useState('name'); const [stackBy, setStackBy] = useState('kind'); @@ -29,7 +23,7 @@ export function ExplorerOptionsPanel() { return (
{/* Explorer Appearance */} - Item size + Item size { getExplorerStore().gridItemSize = value[0] || 100; @@ -42,7 +36,7 @@ export function ExplorerOptionsPanel() { />
- Sort by + Sort by
- Stack by + Stack by { - ref(el); + return ( +
null)} className="relative flex h-7"> + { + ref(el); - if (typeof forwardedRef === 'function') forwardedRef(el); - else if (forwardedRef) forwardedRef.current = el; - }} - placeholder="Search" - className={clsx('w-32 transition-all focus:w-52', props.className)} - {...searchField} - /> -
- {platform === 'browser' ? ( - - ) : os === 'macOS' ? ( - - ) : ( - - )} -
-
- ); -}); + if (typeof forwardedRef === 'function') forwardedRef(el); + else if (forwardedRef) forwardedRef.current = el; + }} + placeholder="Search" + className={clsx('w-32 transition-all focus:w-52', props.className)} + {...searchField} + /> +
+ {platform === 'browser' ? ( + + ) : os === 'macOS' ? ( + + ) : ( + + )} +
+ + ); + } +); -export type TopBarProps = DefaultProps & { +export type TopBarProps = { showSeparator?: boolean; }; -export const TopBar: React.FC = (props) => { +export default (props: TopBarProps) => { const platform = useOperatingSystem(false); const os = useOperatingSystem(true); @@ -235,8 +233,8 @@ export const TopBar: React.FC = (props) => { (getExplorerStore().layoutMode = 'list')} + active={store.layoutMode === 'rows'} + onClick={() => (getExplorerStore().layoutMode = 'rows')} > @@ -326,7 +324,7 @@ export const TopBar: React.FC = (props) => { } >
- +
@@ -342,7 +340,7 @@ export const TopBar: React.FC = (props) => { > diff --git a/packages/interface/src/components/explorer/VirtualizedList.tsx b/interface/app/$libraryId/Explorer/VirtualizedList.tsx similarity index 89% rename from packages/interface/src/components/explorer/VirtualizedList.tsx rename to interface/app/$libraryId/Explorer/VirtualizedList.tsx index c3dfc0408..f4462ab17 100644 --- a/packages/interface/src/components/explorer/VirtualizedList.tsx +++ b/interface/app/$libraryId/Explorer/VirtualizedList.tsx @@ -4,12 +4,11 @@ import { useSearchParams } from 'react-router-dom'; import { useKey, useOnWindowResize } from 'rooks'; import { ExplorerContext, ExplorerItem, isPath } from '@sd/client'; import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore'; -import { LIST_VIEW_HEADER_HEIGHT, ListViewHeader } from './FileColumns'; -import FileItem from './FileItem'; -import FileRow from './FileRow'; +import FileItem from './File/Item'; +import FileRow from './File/Row'; +import { ROW_HEADER_HEIGHT, RowHeader } from './File/RowHeader'; const TOP_BAR_HEIGHT = 46; -// const GRID_TEXT_AREA_HEIGHT = 25; interface Props { context: ExplorerContext; @@ -17,7 +16,7 @@ interface Props { onScroll?: (posY: number) => void; } -export const VirtualizedList = memo(({ data, context, onScroll }: Props) => { +export const VirtualizedList = memo(({ data, onScroll }: Props) => { const scrollRef = useRef(null); const innerRef = useRef(null); @@ -64,7 +63,7 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => { getScrollElement: () => scrollRef.current, overscan: 200, estimateSize: () => itemSize, - measureElement: (index) => itemSize + measureElement: () => itemSize }); // TODO: Make scroll adjustment work with both list and grid layout, currently top bar offset disrupts positioning of list, and grid just doesn't work @@ -101,20 +100,17 @@ export const VirtualizedList = memo(({ data, context, onScroll }: Props) => {
{ - getExplorerStore().selectedRowIndex = -1; - }} + onClick={() => (getExplorerStore().selectedRowIndex = -1)} >
- {layoutMode === 'list' && } + {layoutMode === 'rows' && } {rowVirtualizer.getVirtualItems().map((virtualRow) => (
{ transform: `translateY(${virtualRow.start}px)` }} > - {layoutMode === 'list' && ( + {layoutMode === 'rows' && ( ) => { const [open, setOpen] = useState(false); @@ -87,7 +81,7 @@ export const KeyDropdown = ({ ); }; -export const Key: React.FC<{ data: Key; index: number }> = ({ data, index }) => { +export const Key = ({ data }: { data: Key }) => { const mountKey = useLibraryMutation('keys.mount'); const unmountKey = useLibraryMutation('keys.unmount'); const deleteKey = useLibraryMutation('keys.deleteFromLibrary'); diff --git a/interface/app/$libraryId/KeyManager/List.tsx b/interface/app/$libraryId/KeyManager/List.tsx new file mode 100644 index 000000000..e0e8e19b5 --- /dev/null +++ b/interface/app/$libraryId/KeyManager/List.tsx @@ -0,0 +1,56 @@ +import { useMemo, useRef } from 'react'; +import { useLibraryQuery } from '@sd/client'; +import { SelectOption } from '@sd/ui'; +import { DummyKey, Key } from './Key'; + +// ideal for going within a select box +// can use mounted or unmounted keys, just provide different inputs +export const KeyListSelectOptions = (props: { keys: string[] }) => ( + <> + {props.keys.map((key) => ( + + Key {key.substring(0, 8).toUpperCase()} + + ))} + +); + +export default () => { + const keys = useLibraryQuery(['keys.list']); + const mountedUuids = useLibraryQuery(['keys.listMounted']); + const defaultKey = useLibraryQuery(['keys.getDefault']); + + const mountingQueue = useRef(new Set()); + + const [mountedKeys, unmountedKeys] = useMemo( + () => [ + keys.data?.filter((key) => mountedUuids.data?.includes(key.uuid)) ?? [], + keys.data?.filter((key) => !mountedUuids.data?.includes(key.uuid)) ?? [] + ], + [keys, mountedUuids] + ); + + if (keys.data?.length === 0) { + return ; + } + + return ( + <> + {[...mountedKeys, ...unmountedKeys]?.map((key) => ( + + ))} + + ); +}; diff --git a/packages/interface/src/components/key/KeyMounter.tsx b/interface/app/$libraryId/KeyManager/Mounter.tsx similarity index 86% rename from packages/interface/src/components/key/KeyMounter.tsx rename to interface/app/$libraryId/KeyManager/Mounter.tsx index 2c351f656..ba928a0a2 100644 --- a/packages/interface/src/components/key/KeyMounter.tsx +++ b/interface/app/$libraryId/KeyManager/Mounter.tsx @@ -1,19 +1,18 @@ -import cryptoRandomString from 'crypto-random-string'; import { Eye, EyeSlash, Info } from 'phosphor-react'; import { useEffect, useRef, useState } from 'react'; -import { Algorithm, useLibraryMutation } from '@sd/client'; -import { Button, CategoryHeading, Input, Select, SelectOption, Switch, tw } from '@sd/ui'; -import { getHashingAlgorithmSettings } from '../../screens/settings/library/KeysSetting'; -import Slider from '../primitive/Slider'; -import { Tooltip } from '../tooltip/Tooltip'; +import { + Algorithm, + HASHING_ALGOS, + HashingAlgoSlug, + generatePassword, + useLibraryMutation +} from '@sd/client'; +import { Button, CategoryHeading, Input, Select, SelectOption, Slider, Switch, tw } from '@sd/ui'; +import { Tooltip } from '@sd/ui'; const KeyHeading = tw(CategoryHeading)`mb-1`; -export const generatePassword = (length: number) => { - return cryptoRandomString({ length, type: 'ascii-printable' }); -}; - -export function KeyMounter() { +export default () => { const ref = useRef(null); const [showKey, setShowKey] = useState(false); const [librarySync, setLibrarySync] = useState(true); @@ -23,7 +22,7 @@ export function KeyMounter() { const [key, setKey] = useState(''); const [encryptionAlgo, setEncryptionAlgo] = useState('XChaCha20Poly1305'); - const [hashingAlgo, setHashingAlgo] = useState('Argon2id-s'); + const [hashingAlgo, setHashingAlgo] = useState('Argon2id-s'); const createKey = useLibraryMutation('keys.add'); const CurrentEyeIcon = showKey ? EyeSlash : Eye; @@ -123,7 +122,11 @@ export function KeyMounter() {
Hashing - setHashingAlgo(s as HashingAlgoSlug)} + value={hashingAlgo} + > Argon2id (standard) Argon2id (hardened) Argon2id (paranoid) @@ -140,7 +143,7 @@ export function KeyMounter() { onClick={() => { setKey(''); - const hashing_algorithm = getHashingAlgorithmSettings(hashingAlgo); + const hashing_algorithm = HASHING_ALGOS[hashingAlgo]; createKey.mutate({ algorithm: encryptionAlgo as Algorithm, @@ -155,4 +158,4 @@ export function KeyMounter() {
); -} +}; diff --git a/interface/app/$libraryId/KeyManager/NotUnlocked.tsx b/interface/app/$libraryId/KeyManager/NotUnlocked.tsx new file mode 100644 index 000000000..c2cee1d8d --- /dev/null +++ b/interface/app/$libraryId/KeyManager/NotUnlocked.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import { useLibraryMutation, useLibraryQuery } from '@sd/client'; +import { Button, PasswordInput } from '@sd/ui'; +import { showAlertDialog } from '~/components/AlertDialog'; + +export default () => { + const keyringSk = useLibraryQuery(['keys.getSecretKey'], { initialData: '' }); + const unlockKeyManager = useLibraryMutation('keys.unlockKeyManager', { + onError: () => + showAlertDialog({ + title: 'Unlock Error', + value: 'The information provided to the key manager was incorrect' + }) + }); + const isKeyManagerUnlocking = useLibraryQuery(['keys.isKeyManagerUnlocking']); + + const [masterPassword, setMasterPassword] = useState(''); + const [secretKey, setSecretKey] = useState(''); + + const [enterSkManually, setEnterSkManually] = useState(keyringSk?.data === null); + + return ( +
+ setMasterPassword(e.target.value)} + autoFocus + /> + + {enterSkManually && ( + setSecretKey(e.target.value)} + autoFocus + /> + )} + + + + {!enterSkManually && ( +

setEnterSkManually(true)}> + or enter secret key manually +

+ )} +
+ ); +}; diff --git a/interface/app/$libraryId/KeyManager/index.tsx b/interface/app/$libraryId/KeyManager/index.tsx new file mode 100644 index 000000000..fb254e021 --- /dev/null +++ b/interface/app/$libraryId/KeyManager/index.tsx @@ -0,0 +1,95 @@ +import { Gear, Lock } from 'phosphor-react'; +import { useLibraryContext, useLibraryMutation, useLibraryQuery } from '@sd/client'; +import { Button, ButtonLink, Tabs } from '@sd/ui'; +import KeyList from './List'; +import KeyMounter from './Mounter'; +import NotUnlocked from './NotUnlocked'; + +export function KeyManager() { + const isUnlocked = useLibraryQuery(['keys.isUnlocked']); + + if (!isUnlocked?.data) return ; + else return ; +} + +const Unlocked = () => { + const { library } = useLibraryContext(); + + const unmountAll = useLibraryMutation('keys.unmountAll'); + const clearMasterPassword = useLibraryMutation('keys.clearMasterPassword'); + + return ( +
+ +
+ + + Mount + + + Keys + +
+ + + + + +
+ + + + + + + +
+ ); +}; + +const Keys = () => { + const unmountAll = useLibraryMutation(['keys.unmountAll']); + + return ( +
+
+
+ {/* Mounted keys */} +
+ +
+
+
+
+ +
+ +
+
+ ); +}; diff --git a/interface/app/$libraryId/Layout/Sidebar/AddLocationButton.tsx b/interface/app/$libraryId/Layout/Sidebar/AddLocationButton.tsx new file mode 100644 index 000000000..27ed29c77 --- /dev/null +++ b/interface/app/$libraryId/Layout/Sidebar/AddLocationButton.tsx @@ -0,0 +1,40 @@ +import { useLibraryMutation } from '@sd/client'; +import { dialogManager } from '@sd/ui'; +import { usePlatform } from '~/util/Platform'; +import AddLocationDialog from '../../settings/library/locations/AddDialog'; + +export default () => { + const platform = usePlatform(); + + const createLocation = useLibraryMutation('locations.create'); + + return ( + + ); +}; diff --git a/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx b/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx new file mode 100644 index 000000000..c4e5f821f --- /dev/null +++ b/interface/app/$libraryId/Layout/Sidebar/DebugPopover.tsx @@ -0,0 +1,85 @@ +import { getDebugState, useBridgeQuery, useDebugState } from '@sd/client'; +import { Button, Popover, Select, SelectOption, Switch } from '@sd/ui'; +import { usePlatform } from '~/util/Platform'; +import Setting from '../../settings/Setting'; + +export default () => { + const buildInfo = useBridgeQuery(['buildInfo']); + const nodeState = useBridgeQuery(['nodeState']); + + const debugState = useDebugState(); + const platform = usePlatform(); + + return ( + + v{buildInfo.data?.version || '-.-.-'} - {buildInfo.data?.commit || 'dev'} + + } + > +
+ + (getDebugState().rspcLogger = !debugState.rspcLogger)} + /> + + {platform.openPath && ( + +
+ +
+
+ )} + + + + + {/* {platform.showDevtools && ( + +
+ +
+
+ )} */} +
+
+ ); +}; diff --git a/interface/app/$libraryId/Layout/Sidebar/Icon.tsx b/interface/app/$libraryId/Layout/Sidebar/Icon.tsx new file mode 100644 index 000000000..ecccdc8bd --- /dev/null +++ b/interface/app/$libraryId/Layout/Sidebar/Icon.tsx @@ -0,0 +1,5 @@ +import clsx from 'clsx'; + +export default ({ component: Icon, ...props }: any) => ( + +); diff --git a/packages/interface/src/components/jobs/JobManager.tsx b/interface/app/$libraryId/Layout/Sidebar/JobManager.tsx similarity index 92% rename from packages/interface/src/components/jobs/JobManager.tsx rename to interface/app/$libraryId/Layout/Sidebar/JobManager.tsx index fe1fd415b..c5bf73cfd 100644 --- a/packages/interface/src/components/jobs/JobManager.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/JobManager.tsx @@ -4,7 +4,6 @@ import { ArrowsClockwise, Camera, Copy, - DotsThree, Eye, Fingerprint, Folder, @@ -18,9 +17,7 @@ import { X } from 'phosphor-react'; import { JobReport, useLibraryMutation, useLibraryQuery } from '@sd/client'; -import { Button, CategoryHeading, Popover, PopoverClose, tw } from '@sd/ui'; -import ProgressBar from '../primitive/ProgressBar'; -import { Tooltip } from '../tooltip/Tooltip'; +import { Button, CategoryHeading, PopoverClose, ProgressBar, Tooltip } from '@sd/ui'; interface JobNiceData { name: string; @@ -93,12 +90,6 @@ const StatusColors: Record = { Paused: 'text-gray-500' }; -function elapsed(seconds: number) { - return new Date(seconds * 1000).toUTCString().match(/(\d\d:\d\d:\d\d)/)?.[0]; -} - -const HeaderContainer = tw.div`z-20 flex items-center w-full h-10 px-2 border-b border-app-line/50 rounded-t-md bg-app-button/70`; - export function JobsManager() { const runningJobs = useLibraryQuery(['jobs.getRunning']); const jobs = useLibraryQuery(['jobs.getHistory']); @@ -106,7 +97,7 @@ export function JobsManager() { return (
- +
Recent Jobs
@@ -122,7 +113,7 @@ export function JobsManager() { - +
diff --git a/interface/app/$libraryId/Layout/Sidebar/LibrariesDropdown.tsx b/interface/app/$libraryId/Layout/Sidebar/LibrariesDropdown.tsx new file mode 100644 index 000000000..c8e061b04 --- /dev/null +++ b/interface/app/$libraryId/Layout/Sidebar/LibrariesDropdown.tsx @@ -0,0 +1,59 @@ +import clsx from 'clsx'; +import { Gear, Lock, Plus } from 'phosphor-react'; +import { useClientContext } from '@sd/client'; +import { Dropdown, dialogManager } from '@sd/ui'; +import CreateDialog from '../../settings/node/libraries/CreateDialog'; + +export default () => { + const { library, libraries, currentLibraryId } = useClientContext(); + + return ( + + + {libraries.isLoading ? 'Loading...' : library ? library.config.name : ' '} + + + } + > + + {libraries.data?.map((lib) => ( + + {lib.config.name} + + ))} + + + dialogManager.create((dp) => )} + > + New Library + + + Manage Library + + alert('TODO: Not implemented yet!')}> + Lock + + + + ); +}; diff --git a/interface/app/$libraryId/Layout/Sidebar/Link.tsx b/interface/app/$libraryId/Layout/Sidebar/Link.tsx new file mode 100644 index 000000000..31c7d5e05 --- /dev/null +++ b/interface/app/$libraryId/Layout/Sidebar/Link.tsx @@ -0,0 +1,36 @@ +import { cva } from 'class-variance-authority'; +import clsx from 'clsx'; +import { PropsWithChildren } from 'react'; +import { NavLink, NavLinkProps } from 'react-router-dom'; +import { useOperatingSystem } from '~/hooks/useOperatingSystem'; + +const styles = cva( + 'max-w ring-offset-sidebar focus:ring-accent flex grow flex-row items-center gap-0.5 truncate rounded px-2 py-1 text-sm font-medium outline-none focus:ring-2 focus:ring-offset-2', + { + variants: { + active: { + true: 'bg-sidebar-selected/40 text-ink', + false: 'text-ink-dull' + }, + transparent: { + true: 'bg-opacity-90', + false: '' + } + } + } +); + +export default (props: PropsWithChildren) => { + const os = useOperatingSystem(); + + return ( + + clsx(styles({ active: isActive, transparent: os === 'macOS' }), props.className) + } + > + {props.children} + + ); +}; diff --git a/interface/app/$libraryId/Layout/Sidebar/Section.tsx b/interface/app/$libraryId/Layout/Sidebar/Section.tsx new file mode 100644 index 000000000..928d30bde --- /dev/null +++ b/interface/app/$libraryId/Layout/Sidebar/Section.tsx @@ -0,0 +1,19 @@ +import { PropsWithChildren } from 'react'; +import { CategoryHeading } from '@sd/ui'; + +export default ( + props: PropsWithChildren<{ + name: string; + actionArea?: React.ReactNode; + }> +) => ( +
+
+ {props.name} +
+ {props.actionArea} +
+
+ {props.children} +
+); diff --git a/interface/app/$libraryId/Layout/Sidebar/index.tsx b/interface/app/$libraryId/Layout/Sidebar/index.tsx new file mode 100644 index 000000000..059ed45d9 --- /dev/null +++ b/interface/app/$libraryId/Layout/Sidebar/index.tsx @@ -0,0 +1,259 @@ +import clsx from 'clsx'; +import { + ArchiveBox, + Broadcast, + CheckCircle, + CirclesFour, + CopySimple, + Crosshair, + Eraser, + FilmStrip, + Gear, + MonitorPlay, + Planet +} from 'phosphor-react'; +import { useEffect } from 'react'; +import { Link, NavLink } from 'react-router-dom'; +import { + arraysEqual, + useClientContext, + useDebugState, + useLibraryQuery, + useOnlineLocations +} from '@sd/client'; +import { Button, ButtonLink, Folder, Loader, Popover, Tooltip } from '@sd/ui'; +import { SubtleButton } from '~/components/SubtleButton'; +import { MacTrafficLights } from '~/components/TrafficLights'; +import { useOperatingSystem } from '~/hooks/useOperatingSystem'; +import { OperatingSystem, usePlatform } from '~/util/Platform'; +import AddLocationButton from './AddLocationButton'; +import DebugPopover from './DebugPopover'; +import Icon from './Icon'; +import { JobsManager } from './JobManager'; +import LibrariesDropdown from './LibrariesDropdown'; +import SidebarLink from './Link'; +import Section from './Section'; + +export default () => { + const os = useOperatingSystem(); + + useEffect(() => { + // Prevent the dropdown button to be auto focused on launch + // Hacky but it works + setTimeout(() => { + if (!document.activeElement || !('blur' in document.activeElement)) return; + + (document.activeElement.blur as () => void)(); + }); + }, []); + + return ( +
+ + + +
+
+ ); +}; + +const WindowControls = () => { + const { platform } = usePlatform(); + const os = useOperatingSystem(); + + const showControls = window.location.search.includes('showControls'); + + if (platform === 'tauri' || showControls) { + return ( +
+ {/* We do not provide the onClick handlers for 'MacTrafficLights' because this is only used in demo mode */} + {showControls && } +
+ ); + } + + return null; +}; + +const LibrarySection = () => { + const locations = useLibraryQuery(['locations.list'], { keepPreviousData: true }); + const tags = useLibraryQuery(['tags.list'], { keepPreviousData: true }); + const onlineLocations = useOnlineLocations(); + + return ( + <> +
+ + + } + > + {locations.data?.map((location) => { + const online = onlineLocations?.some((l) => arraysEqual(location.pub_id, l)); + + return ( + +
+ +
+
+ + {location.name} + + ); + })} + {(locations.data?.length || 0) < 4 && } +
+ {!!tags.data?.length && ( +
+ + + } + > +
+ {tags.data?.slice(0, 6).map((tag, index) => ( + +
+ {tag.name} + + ))} +
+
+ )} + + ); +}; + +const Contents = () => { + const { library } = useClientContext(); + + return ( +
+
+ + + Overview + + + + Spaces + + {/* + + People + */} + + + Media + + + + Spacedrop + + + + Imports + +
+ {library && } +
}> + + + Duplicate Finder + + + + Find a File + + + + Cache Cleaner + + + + Media Encoder + +
+
+
+ ); +}; + +const IsRunningJob = () => { + const { data: isRunningJob } = useLibraryQuery(['jobs.isRunning']); + + return isRunningJob ? ( + + ) : ( + + ); +}; + +const Footer = () => { + const { library } = useClientContext(); + const debugState = useDebugState(); + + return ( +
+
+ + + + + + + {library && ( + + + + )} + + } + > +
+ +
+
+
+ {debugState.enabled && } +
+ ); +}; + +// cute little helper to decrease code clutter +const macOnly = (platform: OperatingSystem | undefined, classnames: string) => + platform === 'macOS' ? classnames : ''; diff --git a/packages/interface/src/components/primitive/Toasts.tsx b/interface/app/$libraryId/Layout/Toasts.tsx similarity index 95% rename from packages/interface/src/components/primitive/Toasts.tsx rename to interface/app/$libraryId/Layout/Toasts.tsx index 1ae59b489..c847987cf 100644 --- a/packages/interface/src/components/primitive/Toasts.tsx +++ b/interface/app/$libraryId/Layout/Toasts.tsx @@ -1,9 +1,9 @@ import * as ToastPrimitive from '@radix-ui/react-toast'; import clsx from 'clsx'; -import { useToasts } from '../../hooks/useToasts'; +import { useToasts } from '~/hooks/useToasts'; -export function Toasts() { - const { toasts, addToast, removeToast } = useToasts(); +export default () => { + const { toasts, removeToast } = useToasts(); return (
@@ -71,4 +71,4 @@ export function Toasts() {
); -} +}; diff --git a/packages/interface/src/AppLayout.tsx b/interface/app/$libraryId/Layout/index.tsx similarity index 76% rename from packages/interface/src/AppLayout.tsx rename to interface/app/$libraryId/Layout/index.tsx index 8ed6956d0..8961378bc 100644 --- a/packages/interface/src/AppLayout.tsx +++ b/interface/app/$libraryId/Layout/index.tsx @@ -1,13 +1,12 @@ import clsx from 'clsx'; import { Suspense } from 'react'; -import { Navigate, Outlet } from 'react-router-dom'; +import { Navigate, Outlet, useParams } from 'react-router-dom'; import { ClientContextProvider, LibraryContextProvider, useClientContext } from '@sd/client'; -import { Sidebar } from '~/components/layout/Sidebar'; -import { Toasts } from '~/components/primitive/Toasts'; import { useOperatingSystem } from '~/hooks/useOperatingSystem'; -import { useLibraryId } from './util'; +import Sidebar from './Sidebar'; +import Toasts from './Toasts'; -function AppLayout() { +const Layout = () => { const { libraries, library } = useClientContext(); const os = useOperatingSystem(); @@ -15,7 +14,7 @@ function AppLayout() { if (library === null && libraries.data) { const firstLibrary = libraries.data[0]; - if (firstLibrary) return ; + if (firstLibrary) return ; else return ; } @@ -49,14 +48,14 @@ function AppLayout() {
); -} +}; export default () => { - const currentLibraryId = useLibraryId(); + const params = useParams<{ libraryId: string }>(); return ( - - + + ); }; diff --git a/interface/app/$libraryId/PageLayout.tsx b/interface/app/$libraryId/PageLayout.tsx new file mode 100644 index 000000000..9651fab10 --- /dev/null +++ b/interface/app/$libraryId/PageLayout.tsx @@ -0,0 +1,36 @@ +import clsx from 'clsx'; +import { PropsWithChildren, RefObject, createContext, useContext, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import { Outlet } from 'react-router'; +import DragRegion from '~/components/DragRegion'; + +const PageLayoutContext = createContext<{ ref: RefObject } | null>(null); + +export default () => { + const ref = useRef(null); + + return ( + +
+ +
+ +
+
+
+ ); +}; + +export const DragChildren = ({ children }: PropsWithChildren) => { + const ctx = useContext(PageLayoutContext); + + if (!ctx) throw new Error('Missing PageLayoutContext'); + + const target = ctx.ref.current; + + if (!target) return null; + + return createPortal(children, target); +}; diff --git a/packages/interface/src/screens/Debug.tsx b/interface/app/$libraryId/debug.tsx similarity index 59% rename from packages/interface/src/screens/Debug.tsx rename to interface/app/$libraryId/debug.tsx index ac80e73c0..32ae3126d 100644 --- a/packages/interface/src/screens/Debug.tsx +++ b/interface/app/$libraryId/debug.tsx @@ -1,7 +1,6 @@ import { useBridgeQuery, useLibraryMutation, useLibraryQuery } from '@sd/client'; -import CodeBlock from '~/components/primitive/Codeblock'; +import { CodeBlock } from '~/components/Codeblock'; import { usePlatform } from '~/util/Platform'; -import { ScreenContainer } from './_Layout'; // TODO: Bring this back with a button in the sidebar near settings at the bottom export default function DebugScreen() { @@ -17,10 +16,9 @@ export default function DebugScreen() { // }); const { mutate: identifyFiles } = useLibraryMutation('jobs.identifyUniqueFiles'); return ( - -
-

Developer Debugger

- {/*
+
+

Developer Debugger

+ {/*
*/} -

Running Jobs

- -

Job History

- -

Node State

- -

Libraries

- -
- +

Running Jobs

+ +

Job History

+ +

Node State

+ +

Libraries

+ +
); } diff --git a/interface/app/$libraryId/index.tsx b/interface/app/$libraryId/index.tsx new file mode 100644 index 000000000..93c6e1b75 --- /dev/null +++ b/interface/app/$libraryId/index.tsx @@ -0,0 +1,28 @@ +import { RouteObject } from 'react-router-dom'; +import { lazyEl } from '~/util'; +import settingsRoutes from './settings'; + +export default [ + { + element: lazyEl(() => import("./PageLayout")), + children: [ + { + path: 'overview', + element: lazyEl(() => import('./overview')) + }, + { path: 'people', element: lazyEl(() => import('./people'))}, + { path: 'media', element: lazyEl(() => import('./media')) }, + { path: 'spaces', element: lazyEl(() => import('./spaces')) }, + { path: 'debug', element: lazyEl(() => import('./debug')) }, + { path: 'spacedrop', element: lazyEl(() => import('./spacedrop')) }, + ] + }, + { path: 'location/:id', element: lazyEl(() => import('./location/$id')) }, + { path: 'tag/:id', element: lazyEl(() => import('./tag/$id')) }, + { + path: 'settings', + element: lazyEl(() => import('./settings/Layout')), + children: settingsRoutes + }, + { path: '*', element: lazyEl(() => import('./404')) } +] satisfies RouteObject[]; diff --git a/packages/interface/src/screens/LocationExplorer.tsx b/interface/app/$libraryId/location/$id.tsx similarity index 90% rename from packages/interface/src/screens/LocationExplorer.tsx rename to interface/app/$libraryId/location/$id.tsx index 3f2ff0724..fba3244df 100644 --- a/packages/interface/src/screens/LocationExplorer.tsx +++ b/interface/app/$libraryId/location/$id.tsx @@ -1,8 +1,8 @@ import { useEffect } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; import { useLibraryQuery } from '@sd/client'; -import Explorer from '~/components/explorer/Explorer'; import { getExplorerStore } from '~/hooks/useExplorerStore'; +import Explorer from '../Explorer'; export function useExplorerParams() { const { id } = useParams<{ id?: string }>(); @@ -15,7 +15,7 @@ export function useExplorerParams() { return { location_id, path, limit }; } -export default function LocationExplorer() { +export default () => { const { location_id, path } = useExplorerParams(); useEffect(() => { @@ -39,4 +39,4 @@ export default function LocationExplorer() {
); -} +}; diff --git a/interface/app/$libraryId/media.tsx b/interface/app/$libraryId/media.tsx new file mode 100644 index 000000000..0878acf48 --- /dev/null +++ b/interface/app/$libraryId/media.tsx @@ -0,0 +1,5 @@ +import { ScreenHeading } from '@sd/ui'; + +export default function MediaScreen() { + return Media; +} diff --git a/packages/interface/src/screens/Overview.tsx b/interface/app/$libraryId/overview.tsx similarity index 61% rename from packages/interface/src/screens/Overview.tsx rename to interface/app/$libraryId/overview.tsx index 2cd563cbd..64516fc71 100644 --- a/packages/interface/src/screens/Overview.tsx +++ b/interface/app/$libraryId/overview.tsx @@ -1,4 +1,3 @@ -import { useQueryClient } from '@tanstack/react-query'; import byteSize from 'byte-size'; import clsx from 'clsx'; import { @@ -14,13 +13,10 @@ import { } from 'phosphor-react'; import Skeleton from 'react-loading-skeleton'; import 'react-loading-skeleton/dist/skeleton.css'; -import { Statistics, useLibraryQuery } from '@sd/client'; +import { Statistics, useLibraryContext, useLibraryQuery } from '@sd/client'; import { Card } from '@sd/ui'; import useCounter from '~/hooks/useCounter'; -import { useLibraryId } from '~/util'; import { usePlatform } from '~/util/Platform'; -import { ScreenContainer } from './_Layout'; -import { useEffect } from 'react'; interface StatItemProps { title: string; @@ -91,7 +87,7 @@ const StatItem = (props: StatItemProps) => { export default function OverviewScreen() { const platform = usePlatform(); - const libraryId = useLibraryId(); + const { library } = useLibraryContext(); const stats = useLibraryQuery(['library.getStatistics'], { initialData: { ...EMPTY_STATISTICS } @@ -100,45 +96,43 @@ export default function OverviewScreen() { overviewMounted = true; return ( - -
- {/* STAT HEADER */} -
- {/* STAT CONTAINER */} -
- {Object.entries(stats?.data || []).map(([key, value]) => { - if (!displayableStatItems.includes(key)) return null; - return ( - - ); - })} -
-
+
+ {/* STAT HEADER */} +
+ {/* STAT CONTAINER */} +
+ {Object.entries(stats?.data || []).map(([key, value]) => { + if (!displayableStatItems.includes(key)) return null; + return ( + + ); + })}
-
- - - - - - - - - - -
- - Note:   This is a pre-alpha build of Spacedrive, many features are yet to be - functional. - -
+
- +
+ + + + + + + + + + +
+ + Note:   This is a pre-alpha build of Spacedrive, many features are yet to be + functional. + +
+
); } diff --git a/interface/app/$libraryId/people.tsx b/interface/app/$libraryId/people.tsx new file mode 100644 index 000000000..fcb6958ba --- /dev/null +++ b/interface/app/$libraryId/people.tsx @@ -0,0 +1,5 @@ +import { ScreenHeading } from '@sd/ui'; + +export default () => { + return People; +}; diff --git a/interface/app/$libraryId/settings/Layout.tsx b/interface/app/$libraryId/settings/Layout.tsx new file mode 100644 index 000000000..344681477 --- /dev/null +++ b/interface/app/$libraryId/settings/Layout.tsx @@ -0,0 +1,46 @@ +import { PropsWithChildren, ReactNode, Suspense } from 'react'; +import { Outlet } from 'react-router'; +import { useOperatingSystem } from '~/hooks/useOperatingSystem'; +import DragRegion from '../../../components/DragRegion'; +import Sidebar from './Sidebar'; + +export default () => { + const os = useOperatingSystem(); + + return ( +
+ +
+ {os !== 'browser' ? ( +
+ ) : ( +
+ )} + + + + +
+
+ ); +}; + +interface HeaderProps extends PropsWithChildren { + title: string; + description: string | ReactNode; + rightArea?: ReactNode; +} + +export const Heading = (props: HeaderProps) => { + return ( +
+ {props.children} +
+

{props.title}

+

{props.description}

+
+ {props.rightArea} +
+
+ ); +}; diff --git a/interface/app/$libraryId/settings/ModalLayout.tsx b/interface/app/$libraryId/settings/ModalLayout.tsx new file mode 100644 index 000000000..ddef5fe55 --- /dev/null +++ b/interface/app/$libraryId/settings/ModalLayout.tsx @@ -0,0 +1,45 @@ +import { CaretLeft } from 'phosphor-react'; +import { PropsWithChildren } from 'react'; +import { useNavigate } from 'react-router'; +import { Button, Divider, tw } from '@sd/ui'; + +interface Props extends PropsWithChildren { + title: string; + topRight?: React.ReactNode; +} + +const PageOuter = tw.div`flex h-screen flex-col m-3 -mt-4`; +const Page = tw.div`flex-1 w-full border rounded-md shadow-md shadow-app-shade/30 border-app-box bg-app-box/20`; +const PageInner = tw.div`flex flex-col max-w-4xl w-full h-screen py-6`; +const HeaderArea = tw.div`flex flex-row px-8 items-center space-x-4 mb-2`; +const ContentContainer = tw.div`px-8 pt-5 -mt-1 space-y-6 custom-scroll page-scroll`; + +export default ({ children, title, topRight }: Props) => ( + + + + + +

{title}

+ {topRight} +
+
+ +
+ {children} +
+
+
+); + +const BackButton = () => { + const navigate = useNavigate(); + + return ( + + ); +}; diff --git a/interface/app/$libraryId/settings/OverviewLayout.tsx b/interface/app/$libraryId/settings/OverviewLayout.tsx new file mode 100644 index 000000000..aad387c1d --- /dev/null +++ b/interface/app/$libraryId/settings/OverviewLayout.tsx @@ -0,0 +1,10 @@ +import { Outlet } from 'react-router'; + +export default () => ( +
+
+ +
+
+
+); diff --git a/packages/interface/src/components/primitive/InputContainer.tsx b/interface/app/$libraryId/settings/Setting.tsx similarity index 59% rename from packages/interface/src/components/primitive/InputContainer.tsx rename to interface/app/$libraryId/settings/Setting.tsx index 325866d66..8fc9a81bd 100644 --- a/packages/interface/src/components/primitive/InputContainer.tsx +++ b/interface/app/$libraryId/settings/Setting.tsx @@ -1,17 +1,17 @@ import clsx from 'clsx'; import { PropsWithChildren } from 'react'; -import { DefaultProps } from './types'; -interface InputContainerProps extends DefaultProps { +interface Props { title: string; description?: string; mini?: boolean; + className?: string; } -export function InputContainer({ mini, ...props }: PropsWithChildren) { +export default ({ mini, ...props }: PropsWithChildren) => { return (
-
+

{props.title}

{!!props.description &&

{props.description}

} {!mini && props.children} @@ -19,4 +19,4 @@ export function InputContainer({ mini, ...props }: PropsWithChildren ); -} +}; diff --git a/interface/app/$libraryId/settings/Sidebar.tsx b/interface/app/$libraryId/settings/Sidebar.tsx new file mode 100644 index 000000000..eee8b77b8 --- /dev/null +++ b/interface/app/$libraryId/settings/Sidebar.tsx @@ -0,0 +1,108 @@ +import { + Books, + FlyingSaucer, + GearSix, + Graph, + HardDrive, + Heart, + Key, + KeyReturn, + PaintBrush, + PuzzlePiece, + Receipt, + ShareNetwork, + ShieldCheck, + TagSimple +} from 'phosphor-react'; +import { tw } from '@sd/ui'; +import { useOperatingSystem } from '~/hooks/useOperatingSystem'; +import Icon from '../Layout/Sidebar/Icon'; +import SidebarLink from '../Layout/Sidebar/Link'; + +const Heading = tw.div`mb-1 ml-1 text-xs font-semibold text-gray-400`; +const Section = tw.div`space-y-0.5`; + +export default () => { + const os = useOperatingSystem(); + + return ( +
+ {os !== 'browser' ? ( +
+ ) : ( +
+ )} +
+
+ Client + + + General + + + + Libraries + + + + Privacy + + + + Appearance + + + + Keybinds + + + + Extensions + +
+
+ Library + + + General + + + + Nodes + + + + Locations + + + + Tags + + + + Keys + +
+
+ Resources + + + About + + + + Changelog + + + + Dependencies + + + + Support + +
+
+
+ ); +}; diff --git a/interface/app/$libraryId/settings/client/appearance.tsx b/interface/app/$libraryId/settings/client/appearance.tsx new file mode 100644 index 000000000..ba91fe707 --- /dev/null +++ b/interface/app/$libraryId/settings/client/appearance.tsx @@ -0,0 +1,55 @@ +import { useEffect } from 'react'; +import { forms } from '@sd/ui'; +import { Heading } from '../Layout'; +import Setting from '../Setting'; + +const { Form, Switch, useZodForm, z } = forms; + +const schema = z.object({ + uiAnimations: z.boolean(), + syncThemeWithSystem: z.boolean(), + blurEffects: z.boolean() +}); + +export default function AppearanceSettings() { + const form = useZodForm({ + schema + }); + + const onSubmit = form.handleSubmit(async (data) => { + console.log({ data }); + }); + + useEffect(() => { + const subscription = form.watch(() => onSubmit()); + return () => subscription.unsubscribe(); + }, [form, onSubmit]); + + return ( +
+ + + + + + + + + + + + + ); +} diff --git a/packages/interface/src/screens/settings/client/ExtensionsSettings.tsx b/interface/app/$libraryId/settings/client/extensions.tsx similarity index 85% rename from packages/interface/src/screens/settings/client/ExtensionsSettings.tsx rename to interface/app/$libraryId/settings/client/extensions.tsx index 88a5d808c..bdfc4258e 100644 --- a/packages/interface/src/screens/settings/client/ExtensionsSettings.tsx +++ b/interface/app/$libraryId/settings/client/extensions.tsx @@ -1,7 +1,5 @@ -import { MagnifyingGlass } from 'phosphor-react'; -import { Button, Card, GridLayout, Input, SearchInput } from '@sd/ui'; -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; +import { Button, Card, GridLayout, SearchInput } from '@sd/ui'; +import { Heading } from '../Layout'; // extensions should cache their logos in the app data folder interface ExtensionItemData { @@ -59,8 +57,8 @@ export default function ExtensionSettings() { // const { data: volumes } = useBridgeQuery('GetVolumes'); return ( - - + } @@ -71,6 +69,6 @@ export default function ExtensionSettings() { ))} - + ); } diff --git a/packages/interface/src/screens/settings/client/GeneralSettings.tsx b/interface/app/$libraryId/settings/client/general.tsx similarity index 70% rename from packages/interface/src/screens/settings/client/GeneralSettings.tsx rename to interface/app/$libraryId/settings/client/general.tsx index 2adbccdc6..26698cccb 100644 --- a/packages/interface/src/screens/settings/client/GeneralSettings.tsx +++ b/interface/app/$libraryId/settings/client/general.tsx @@ -1,25 +1,21 @@ import { Database } from 'phosphor-react'; import { getDebugState, useBridgeQuery, useDebugState } from '@sd/client'; import { Card, Input, Switch, tw } from '@sd/ui'; -import { InputContainer } from '~/components/primitive/InputContainer'; -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; import { usePlatform } from '~/util/Platform'; +import { Heading } from '../Layout'; +import Setting from '../Setting'; const NodePill = tw.div`px-1.5 py-[2px] rounded text-xs font-medium bg-app-selected`; const NodeSettingLabel = tw.div`mb-1 text-xs font-medium`; -export default function GeneralSettings() { - const { data: node } = useBridgeQuery(['nodeState']); +export default () => { + const node = useBridgeQuery(['nodeState']); const platform = usePlatform(); const debugState = useDebugState(); return ( - - + <> +
@@ -34,11 +30,22 @@ export default function GeneralSettings() {
Node Name - + { + /* TODO */ + }} + />
Node Port - + { + /* TODO */ + }} + />
@@ -48,8 +55,8 @@ export default function GeneralSettings() {
{ - if (node && platform?.openLink) { - platform.openLink(node.data_path); + if (node.data && platform?.openLink) { + platform.openLink(node.data.data_path); } }} className="text-ink-faint text-sm font-medium" @@ -57,12 +64,12 @@ export default function GeneralSettings() { Data Folder - {node?.data_path} + {node.data?.data_path}
- (getDebugState().enabled = !debugState.enabled)} /> - - + + ); -} +}; diff --git a/interface/app/$libraryId/settings/client/index.ts b/interface/app/$libraryId/settings/client/index.ts new file mode 100644 index 000000000..c788071c0 --- /dev/null +++ b/interface/app/$libraryId/settings/client/index.ts @@ -0,0 +1,10 @@ +import { RouteObject } from "react-router"; +import { lazyEl } from "~/util"; + +export default [ + { path: 'general', element: lazyEl(() => import('./general')) }, + { path: 'appearance', element: lazyEl(() => import('./appearance')) }, + { path: 'keybindings', element: lazyEl(() => import('./keybindings')) }, + { path: 'extensions', element: lazyEl(() => import('./extensions')) }, + { path: 'privacy', element: lazyEl(() => import('./privacy')) }, +] satisfies RouteObject[] diff --git a/packages/interface/src/screens/settings/client/KeybindingSettings.tsx b/interface/app/$libraryId/settings/client/keybindings.tsx similarity index 61% rename from packages/interface/src/screens/settings/client/KeybindingSettings.tsx rename to interface/app/$libraryId/settings/client/keybindings.tsx index cbf33b32c..d2ccf1e2a 100644 --- a/packages/interface/src/screens/settings/client/KeybindingSettings.tsx +++ b/interface/app/$libraryId/settings/client/keybindings.tsx @@ -1,16 +1,15 @@ import { useState } from 'react'; import { Switch } from '@sd/ui'; -import { InputContainer } from '~/components/primitive/InputContainer'; -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; +import { Heading } from '../Layout'; +import Setting from '../Setting'; export default function AppearanceSettings() { const [syncWithLibrary, setSyncWithLibrary] = useState(true); return ( - + <> {/* I don't care what you think the "right" way to write "keybinds" is, I simply refuse to refer to it as "keybindings" */} - - + - - + + ); } diff --git a/packages/interface/src/screens/settings/client/PrivacySettings.tsx b/interface/app/$libraryId/settings/client/privacy.tsx similarity index 56% rename from packages/interface/src/screens/settings/client/PrivacySettings.tsx rename to interface/app/$libraryId/settings/client/privacy.tsx index f13d7334d..ab5ba8301 100644 --- a/packages/interface/src/screens/settings/client/PrivacySettings.tsx +++ b/interface/app/$libraryId/settings/client/privacy.tsx @@ -1,22 +1,22 @@ import { useState } from 'react'; import { Switch } from '@sd/ui'; -import { InputContainer } from '~/components/primitive/InputContainer'; -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; +import { Heading } from '../Layout'; +import Setting from '../Setting'; export default function PrivacySettings() { const [shareUsageData, setShareUsageData] = useState(true); const [blurEffects, setBlurEffects] = useState(true); + return ( - - - + + - - + + ); } diff --git a/interface/app/$libraryId/settings/index.tsx b/interface/app/$libraryId/settings/index.tsx new file mode 100644 index 000000000..17cc645cc --- /dev/null +++ b/interface/app/$libraryId/settings/index.tsx @@ -0,0 +1,28 @@ +import { RouteObject } from 'react-router-dom'; +import { lazyEl } from '~/util'; +import clientRoutes from './client'; +import libraryRoutes from './library'; +import nodeRoutes from './node'; +import resourcesRoutes from './resources'; + +export default [ + { + path: 'client', + element: lazyEl(() => import('./OverviewLayout')), + children: clientRoutes + }, + { + path: 'node', + element: lazyEl(() => import('./OverviewLayout')), + children: nodeRoutes + }, + { + path: 'library', + children: libraryRoutes + }, + { + path: 'resources', + element: lazyEl(() => import('./OverviewLayout')), + children: resourcesRoutes + } +] satisfies RouteObject[]; diff --git a/interface/app/$libraryId/settings/library/backups.tsx b/interface/app/$libraryId/settings/library/backups.tsx new file mode 100644 index 000000000..18bd24db6 --- /dev/null +++ b/interface/app/$libraryId/settings/library/backups.tsx @@ -0,0 +1,9 @@ +import { Heading } from '../Layout'; + +export default () => { + return ( + <> + + + ); +}; diff --git a/interface/app/$libraryId/settings/library/contacts.tsx b/interface/app/$libraryId/settings/library/contacts.tsx new file mode 100644 index 000000000..a2fb70459 --- /dev/null +++ b/interface/app/$libraryId/settings/library/contacts.tsx @@ -0,0 +1,9 @@ +import { Heading } from '../Layout'; + +export default () => { + return ( + <> + + + ); +}; diff --git a/packages/interface/src/screens/settings/library/LibraryGeneralSettings.tsx b/interface/app/$libraryId/settings/library/general.tsx similarity index 77% rename from packages/interface/src/screens/settings/library/LibraryGeneralSettings.tsx rename to interface/app/$libraryId/settings/library/general.tsx index 0eb1a44f2..99f21d0d5 100644 --- a/packages/interface/src/screens/settings/library/LibraryGeneralSettings.tsx +++ b/interface/app/$libraryId/settings/library/general.tsx @@ -1,12 +1,11 @@ import { useForm } from 'react-hook-form'; import { useBridgeMutation, useLibraryContext } from '@sd/client'; import { Button, Input, Switch } from '@sd/ui'; -import { InputContainer } from '~/components/primitive/InputContainer'; -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; import { useDebouncedFormWatch } from '~/hooks/useDebouncedForm'; +import { Heading } from '../Layout'; +import Setting from '../Setting'; -export default function LibraryGeneralSettings() { +export default () => { const { library } = useLibraryContext(); const editLibrary = useBridgeMutation('library.edit'); @@ -23,8 +22,8 @@ export default function LibraryGeneralSettings() { ); return ( - - + @@ -43,7 +42,7 @@ export default function LibraryGeneralSettings() {
-
- - + +
-
- +
- - + + ); -} +}; diff --git a/interface/app/$libraryId/settings/library/index.tsx b/interface/app/$libraryId/settings/library/index.tsx new file mode 100644 index 000000000..25229c12e --- /dev/null +++ b/interface/app/$libraryId/settings/library/index.tsx @@ -0,0 +1,21 @@ +import { RouteObject } from 'react-router'; +import { lazyEl } from '~/util'; + +export default [ + { + element: lazyEl(() => import('../OverviewLayout')), + children: [ + { path: 'contacts', element: lazyEl(() => import('./contacts')) }, + { path: 'keys', element: lazyEl(() => import('./keys')) }, + { path: 'security', element: lazyEl(() => import('./security')) }, + { path: 'sharing', element: lazyEl(() => import('./sharing')) }, + { path: 'sync', element: lazyEl(() => import('./sync')) }, + { path: 'tags', element: lazyEl(() => import('./tags')) }, + { path: 'general', element: lazyEl(() => import('./general')) }, + { path: 'tags', element: lazyEl(() => import('./tags')) }, + { path: 'nodes', element: lazyEl(() => import('./nodes')) }, + { path: 'locations', element: lazyEl(() => import('./locations')) } + ] + }, + { path: 'locations/:id', element: lazyEl(() => import('./locations/$id')) } +] satisfies RouteObject[]; diff --git a/packages/interface/src/components/dialog/BackupRestoreDialog.tsx b/interface/app/$libraryId/settings/library/keys/BackupRestoreDialog.tsx similarity index 94% rename from packages/interface/src/components/dialog/BackupRestoreDialog.tsx rename to interface/app/$libraryId/settings/library/keys/BackupRestoreDialog.tsx index 2206d0e6e..7f5e69e2e 100644 --- a/packages/interface/src/components/dialog/BackupRestoreDialog.tsx +++ b/interface/app/$libraryId/settings/library/keys/BackupRestoreDialog.tsx @@ -3,8 +3,8 @@ import { useState } from 'react'; import { useLibraryMutation } from '@sd/client'; import { Button, Dialog, UseDialogProps, useDialog } from '@sd/ui'; import { forms } from '@sd/ui'; +import { showAlertDialog } from '~/components/AlertDialog'; import { usePlatform } from '~/util/Platform'; -import { showAlertDialog } from '~/util/dialog'; const { Input, useZodForm, z } = forms; @@ -14,9 +14,7 @@ const schema = z.object({ filePath: z.string() }); -export type BackupRestorationDialogProps = UseDialogProps; - -export const BackupRestoreDialog = (props: BackupRestorationDialogProps) => { +export default (props: UseDialogProps) => { const platform = usePlatform(); const restoreKeystoreMutation = useLibraryMutation('keys.restoreKeystore', { diff --git a/packages/interface/src/components/dialog/KeyViewerDialog.tsx b/interface/app/$libraryId/settings/library/keys/KeyViewerDialog.tsx similarity index 85% rename from packages/interface/src/components/dialog/KeyViewerDialog.tsx rename to interface/app/$libraryId/settings/library/keys/KeyViewerDialog.tsx index 76331beca..8ca27c33f 100644 --- a/packages/interface/src/components/dialog/KeyViewerDialog.tsx +++ b/interface/app/$libraryId/settings/library/keys/KeyViewerDialog.tsx @@ -1,13 +1,10 @@ import { Buffer } from 'buffer'; import { Clipboard } from 'phosphor-react'; import { useState } from 'react'; -import { useLibraryQuery } from '@sd/client'; +import { slugFromHashingAlgo, useLibraryQuery } from '@sd/client'; import { Button, Dialog, Input, Select, SelectOption, UseDialogProps, useDialog } from '@sd/ui'; -import { useZodForm, z } from '@sd/ui/src/forms'; -import { getHashingAlgorithmString } from '~/screens/settings/library/KeysSetting'; -import { SelectOptionKeyList } from '../key/KeyList'; - -type KeyViewerDialogProps = UseDialogProps; +import { useZodForm } from '@sd/ui/src/forms'; +import { KeyListSelectOptions } from '~/app/$libraryId/KeyManager/List'; export const KeyUpdater = (props: { uuid: string; @@ -25,17 +22,18 @@ export const KeyUpdater = (props: { const keys = useLibraryQuery(['keys.list']); const key = keys.data?.find((key) => key.uuid == props.uuid); - key && props.setEncryptionAlgo(key?.algorithm); - key && props.setHashingAlgo(getHashingAlgorithmString(key?.hashing_algorithm)); - key && props.setContentSalt(Buffer.from(key.content_salt).toString('hex')); + + if (key) { + props.setEncryptionAlgo(key?.algorithm); + props.setHashingAlgo(slugFromHashingAlgo(key?.hashing_algorithm)); + props.setContentSalt(Buffer.from(key.content_salt).toString('hex')); + } return <>; }; -const schema = z.object({}); - -export const KeyViewerDialog = (props: KeyViewerDialogProps) => { - const form = useZodForm({ schema }); +export default (props: UseDialogProps) => { + const form = useZodForm(); const dialog = useDialog(props); const keys = useLibraryQuery(['keys.list'], { @@ -79,7 +77,7 @@ export const KeyViewerDialog = (props: KeyViewerDialogProps) => { setKey(e); }} > - {keys.data && key.uuid)} />} + {keys.data && key.uuid)} />}
diff --git a/packages/interface/src/components/dialog/MasterPasswordChangeDialog.tsx b/interface/app/$libraryId/settings/library/keys/MasterPasswordDialog.tsx similarity index 86% rename from packages/interface/src/components/dialog/MasterPasswordChangeDialog.tsx rename to interface/app/$libraryId/settings/library/keys/MasterPasswordDialog.tsx index c022d3a48..a722a4275 100644 --- a/packages/interface/src/components/dialog/MasterPasswordChangeDialog.tsx +++ b/interface/app/$libraryId/settings/library/keys/MasterPasswordDialog.tsx @@ -1,24 +1,34 @@ import { ArrowsClockwise, Clipboard, Eye, EyeSlash } from 'phosphor-react'; -import { lazy, useState } from 'react'; -import { Algorithm, useLibraryMutation } from '@sd/client'; -import { Button, Dialog, Input, Select, SelectOption, UseDialogProps, useDialog } from '@sd/ui'; +import { useState } from 'react'; +import { + Algorithm, + HASHING_ALGOS, + HashingAlgoSlug, + generatePassword, + hashingAlgoSlugSchema, + useLibraryMutation +} from '@sd/client'; +import { + Button, + Dialog, + Input, + PasswordMeter, + Select, + SelectOption, + UseDialogProps, + useDialog +} from '@sd/ui'; import { useZodForm, z } from '@sd/ui/src/forms'; -import { getHashingAlgorithmSettings } from '~/screens/settings/library/KeysSetting'; -import { showAlertDialog } from '~/util/dialog'; -import { generatePassword } from '../key/KeyMounter'; - -const PasswordMeter = lazy(() => import('../key/PasswordMeter')); - -export type MasterPasswordChangeDialogProps = UseDialogProps; +import { showAlertDialog } from '~/components/AlertDialog'; const schema = z.object({ masterPassword: z.string(), masterPassword2: z.string(), encryptionAlgo: z.string(), - hashingAlgo: z.string() + hashingAlgo: hashingAlgoSlugSchema }); -export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProps) => { +export default (props: UseDialogProps) => { const changeMasterPassword = useLibraryMutation('keys.changeMasterPassword', { onSuccess: () => { showAlertDialog({ @@ -62,7 +72,8 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp value: 'Passwords are not the same, please try again.' }); } else { - const hashing_algorithm = getHashingAlgorithmSettings(data.hashingAlgo); + const hashing_algorithm = HASHING_ALGOS[data.hashingAlgo]; + return changeMasterPassword.mutateAsync({ algorithm: data.encryptionAlgo as Algorithm, hashing_algorithm, @@ -160,7 +171,7 @@ export const MasterPasswordChangeDialog = (props: MasterPasswordChangeDialogProp +
+ + ); +}; diff --git a/interface/app/$libraryId/settings/library/tags/DeleteDialog.tsx b/interface/app/$libraryId/settings/library/tags/DeleteDialog.tsx new file mode 100644 index 000000000..edce75faf --- /dev/null +++ b/interface/app/$libraryId/settings/library/tags/DeleteDialog.tsx @@ -0,0 +1,29 @@ +import { useLibraryMutation } from '@sd/client'; +import { Dialog, UseDialogProps, useDialog } from '@sd/ui'; +import { useZodForm } from '@sd/ui/src/forms'; + +interface Props extends UseDialogProps { + tagId: number; + onSuccess: () => void; +} + +export default (props: Props) => { + const dialog = useDialog(props); + + const form = useZodForm(); + + const deleteTag = useLibraryMutation('tags.delete', { + onSuccess: props.onSuccess + }); + + return ( + deleteTag.mutateAsync(props.tagId))} + title="Delete Tag" + description="Are you sure you want to delete this tag? This cannot be undone and tagged files will be unlinked." + ctaDanger + ctaLabel="Delete" + /> + ); +}; diff --git a/interface/app/$libraryId/settings/library/tags/EditForm.tsx b/interface/app/$libraryId/settings/library/tags/EditForm.tsx new file mode 100644 index 000000000..579bcbce1 --- /dev/null +++ b/interface/app/$libraryId/settings/library/tags/EditForm.tsx @@ -0,0 +1,70 @@ +import { Trash } from 'phosphor-react'; +import { Tag, useLibraryMutation } from '@sd/client'; +import { Button, Switch, Tooltip, dialogManager } from '@sd/ui'; +import { Form, Input, useZodForm, z } from '@sd/ui/src/forms'; +import ColorPicker from '~/components/ColorPicker'; +import { useDebouncedFormWatch } from '~/hooks/useDebouncedForm'; +import Setting from '../../Setting'; +import DeleteDialog from './DeleteDialog'; + +const schema = z.object({ + name: z.string().nullable(), + color: z.string().nullable() +}); + +interface Props { + tag: Tag; + onDelete: () => void; +} + +export default ({ tag, onDelete }: Props) => { + const updateTag = useLibraryMutation('tags.update'); + + const form = useZodForm({ + schema, + defaultValues: tag + }); + + useDebouncedFormWatch(form, (data) => + updateTag.mutate({ + name: data.name ?? null, + color: data.color ?? null, + id: tag.id + }) + ); + + return ( +
+
+
+ Color +
+ + +
+
+
+ Name + +
+
+ +
+ + + + + ); +}; diff --git a/interface/app/$libraryId/settings/library/tags/index.tsx b/interface/app/$libraryId/settings/library/tags/index.tsx new file mode 100644 index 000000000..f2f8160db --- /dev/null +++ b/interface/app/$libraryId/settings/library/tags/index.tsx @@ -0,0 +1,57 @@ +import clsx from 'clsx'; +import { useState } from 'react'; +import { Tag, useLibraryQuery } from '@sd/client'; +import { Button, Card, dialogManager } from '@sd/ui'; +import { Heading } from '../../Layout'; +import CreateDialog from './CreateDialog'; +import EditForm from './EditForm'; + +export default function TagsSettings() { + const tags = useLibraryQuery(['tags.list']); + + const [selectedTag, setSelectedTag] = useState(tags.data?.[0] ?? null); + + return ( + <> + + +
+ } + /> + +
+ {tags.data?.map((tag) => ( +
setSelectedTag(tag.id === selectedTag?.id ? null : tag)} + key={tag.id} + className={clsx( + 'flex items-center rounded px-1.5 py-0.5', + selectedTag?.id === tag.id && 'ring' + )} + style={{ backgroundColor: tag.color + 'CC' }} + > + {tag.name} +
+ ))} +
+
+ {selectedTag ? ( + setSelectedTag(null)} /> + ) : ( +
No Tag Selected
+ )} + + ); +} diff --git a/interface/app/$libraryId/settings/node/index.tsx b/interface/app/$libraryId/settings/node/index.tsx new file mode 100644 index 000000000..bbd293a46 --- /dev/null +++ b/interface/app/$libraryId/settings/node/index.tsx @@ -0,0 +1,7 @@ +import { RouteObject } from "react-router"; +import { lazyEl } from "~/util"; + +export default [ + { path: 'p2p', element: lazyEl(() => import('./p2p')) }, + { path: 'libraries', element: lazyEl(() => import('./libraries')) }, +] satisfies RouteObject[] diff --git a/packages/interface/src/components/dialog/CreateLibraryDialog.tsx b/interface/app/$libraryId/settings/node/libraries/CreateDialog.tsx similarity index 89% rename from packages/interface/src/components/dialog/CreateLibraryDialog.tsx rename to interface/app/$libraryId/settings/node/libraries/CreateDialog.tsx index 83d19ec89..da8219a25 100644 --- a/packages/interface/src/components/dialog/CreateLibraryDialog.tsx +++ b/interface/app/$libraryId/settings/node/libraries/CreateDialog.tsx @@ -1,13 +1,24 @@ import { useQueryClient } from '@tanstack/react-query'; import { ArrowsClockwise, Clipboard, Eye, EyeSlash } from 'phosphor-react'; -import { lazy, useState } from 'react'; -import { Algorithm, useBridgeMutation } from '@sd/client'; -import { Button, Dialog, Select, SelectOption, UseDialogProps, useDialog } from '@sd/ui'; +import { useState } from 'react'; +import { + Algorithm, + HASHING_ALGOS, + HashingAlgoSlug, + generatePassword, + hashingAlgoSlugSchema, + useBridgeMutation +} from '@sd/client'; +import { + Button, + Dialog, + PasswordMeter, + Select, + SelectOption, + UseDialogProps, + useDialog +} from '@sd/ui'; import { forms } from '@sd/ui'; -import { getHashingAlgorithmSettings } from '~/screens/settings/library/KeysSetting'; -import { generatePassword } from '../key/KeyMounter'; - -const PasswordMeter = lazy(() => import('../key/PasswordMeter')); const { Input, z, useZodForm } = forms; @@ -16,12 +27,10 @@ const schema = z.object({ password: z.string(), password_validate: z.string(), algorithm: z.string(), - hashing_algorithm: z.string() + hashing_algorithm: hashingAlgoSlugSchema }); -type Props = UseDialogProps; - -export default function CreateLibraryDialog(props: Props) { +export default (props: UseDialogProps) => { const dialog = useDialog(props); const form = useZodForm({ @@ -58,7 +67,7 @@ export default function CreateLibraryDialog(props: Props) { await createLibrary.mutateAsync({ ...data, algorithm: data.algorithm as Algorithm, - hashing_algorithm: getHashingAlgorithmSettings(data.hashing_algorithm), + hashing_algorithm: HASHING_ALGOS[data.hashing_algorithm], auth: { type: 'Password', value: data.password @@ -170,7 +179,7 @@ export default function CreateLibraryDialog(props: Props) { setMasterPassword(e.target.value)} - autoFocus - type={showMasterPassword ? 'text' : 'password'} - className="grow !py-0.5" - placeholder="Master Password" - /> - -
- - {enterSkManually && ( -
- setSecretKey(e.target.value)} - type={showSecretKey ? 'text' : 'password'} - className="grow !py-0.5" - placeholder="Secret Key" - /> - -
- )} - - {!enterSkManually && ( -
-

setEnterSkManually(true)}> - or enter secret key manually -

-
- )} -
- ); - } else { - return ( -
- -
- - - Mount - - - Keys - -
- - - - - -
- {isUnlocked && ( - - - - )} - - - - -
- ); - } -} diff --git a/packages/interface/src/components/layout/DragRegion.tsx b/packages/interface/src/components/layout/DragRegion.tsx deleted file mode 100644 index fd9a31bda..000000000 --- a/packages/interface/src/components/layout/DragRegion.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { PropsWithChildren } from 'react'; -import { cx } from '@sd/ui'; - -export default function DragRegion(props: PropsWithChildren & { className?: string }) { - return ( -
- {props.children} -
- ); -} diff --git a/packages/interface/src/components/layout/Modal.tsx b/packages/interface/src/components/layout/Modal.tsx deleted file mode 100644 index 3ae946ea1..000000000 --- a/packages/interface/src/components/layout/Modal.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Transition } from '@headlessui/react'; -import clsx from 'clsx'; -import { X } from 'phosphor-react'; -import { PropsWithChildren } from 'react'; -import { ButtonLink } from '@sd/ui'; - -export function Model( - props: PropsWithChildren<{ - full?: boolean; - }> -) { - return ( -
-
- -
- - - - - -
- {props.children} -
-
-
-
- ); -} diff --git a/packages/interface/src/components/layout/Sidebar.tsx b/packages/interface/src/components/layout/Sidebar.tsx deleted file mode 100644 index 83c3a02f7..000000000 --- a/packages/interface/src/components/layout/Sidebar.tsx +++ /dev/null @@ -1,506 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query'; -import clsx from 'clsx'; -import { - ArchiveBox, - Broadcast, - CheckCircle, - CirclesFour, - CopySimple, - Crosshair, - Eraser, - FilmStrip, - Gear, - Lock, - MonitorPlay, - Planet, - Plus -} from 'phosphor-react'; -import React, { PropsWithChildren, useEffect } from 'react'; -import { Link, NavLink, NavLinkProps, useLocation } from 'react-router-dom'; -import { - Location, - LocationCreateArgs, - arraysEqual, - getDebugState, - useBridgeQuery, - useClientContext, - useDebugState, - useLibraryMutation, - useLibraryQuery, - useOnlineLocations -} from '@sd/client'; -import { - Button, - ButtonLink, - CategoryHeading, - Dropdown, - Loader, - Popover, - Select, - SelectOption, - Switch, - cva, - dialogManager, - tw -} from '@sd/ui'; -import { useOperatingSystem } from '~/hooks/useOperatingSystem'; -import { OperatingSystem, usePlatform } from '~/util/Platform'; -import AddLocationDialog from '../dialog/AddLocationDialog'; -import CreateLibraryDialog from '../dialog/CreateLibraryDialog'; -import { Folder } from '../icons/Folder'; -import { JobsManager } from '../jobs/JobManager'; -import { MacTrafficLights } from '../os/TrafficLights'; -import { InputContainer } from '../primitive/InputContainer'; -import { SubtleButton } from '../primitive/SubtleButton'; -import { Tooltip } from '../tooltip/Tooltip'; - -const SidebarBody = tw.div`flex relative flex-col flex-grow-0 flex-shrink-0 w-44 min-h-full border-r border-sidebar-divider bg-sidebar`; - -const SidebarContents = tw.div`flex flex-col px-2.5 flex-grow pt-1 pb-10 overflow-x-hidden overflow-y-scroll no-scrollbar mask-fade-out`; - -const SidebarFooter = tw.div`flex flex-col mb-3 px-2.5`; - -export function Sidebar() { - // DO NOT DO LIBRARY QUERIES OR MUTATIONS HERE. This is rendered before a library is set. - - const os = useOperatingSystem(); - const { library, libraries, currentLibraryId } = useClientContext(); - const debugState = useDebugState(); - - useEffect(() => { - // Prevent the dropdown button to be auto focused on launch - // Hacky but it works - setTimeout(() => { - if (!document.activeElement || !('blur' in document.activeElement)) return; - - (document.activeElement.blur as () => void)(); - }); - }, []); - - console.log(useLocation()); - - return ( - - - - - {libraries.isLoading ? 'Loading...' : library ? library.config.name : ' '} - - - } - > - - {libraries.data?.map((lib) => ( - - {lib.config.name} - - ))} - - - { - dialogManager.create((dp) => ); - }} - > - New Library - - - Manage Library - - alert('TODO: Not implemented yet!')}> - Lock - - - - -
- - - Overview - - - - Spaces - - {/* - - People - */} - - - Media - - - - Spacedrop - - - - Imports - -
- {library && } - }> - - - Duplicate Finder - - - - Find a File - - - - Cache Cleaner - - - - Media Encoder - - -
- - -
- - - - - - - {library && ( - - - - )} - - } - > -
- -
-
-
- {debugState.enabled && } -
- - ); -} - -function IsRunningJob() { - const { data: isRunningJob } = useLibraryQuery(['jobs.isRunning']); - - return isRunningJob ? ( - - ) : ( - - ); -} - -function DebugPanel() { - const queryClient = useQueryClient(); - const buildInfo = useBridgeQuery(['buildInfo']); - const nodeState = useBridgeQuery(['nodeState']); - const debugState = useDebugState(); - const platform = usePlatform(); - - return ( - - v{buildInfo.data?.version || '-.-.-'} - {buildInfo.data?.commit || 'dev'} - - } - > -
- - (getDebugState().rspcLogger = !debugState.rspcLogger)} - /> - - {platform.openPath && ( - -
- -
-
- )} - - - -
- -
- - {/* {platform.showDevtools && ( - -
- -
-
- )} */} -
-
- ); -} - -const sidebarItemClass = cva( - 'max-w ring-offset-sidebar focus:ring-accent mb-[2px] flex grow flex-row items-center gap-0.5 truncate rounded px-2 py-1 text-sm font-medium outline-none focus:ring-2 focus:ring-offset-2', - { - variants: { - isActive: { - true: 'bg-sidebar-selected/40 text-ink', - false: 'text-ink-dull' - }, - isTransparent: { - true: 'bg-opacity-90', - false: '' - } - } - } -); - -export const SidebarLink = (props: PropsWithChildren) => { - const os = useOperatingSystem(); - return ( - - clsx(sidebarItemClass({ isActive, isTransparent: os === 'macOS' }), props.className) - } - > - {props.children} - - ); -}; - -const SidebarSection = ( - props: PropsWithChildren<{ - name: string; - actionArea?: React.ReactNode; - }> -) => { - return ( -
-
- {props.name} -
- {props.actionArea} -
-
- {props.children} -
- ); -}; - -function LibraryScopedSection() { - const platform = usePlatform(); - - const locations = useLibraryQuery(['locations.list'], { keepPreviousData: true }); - const tags = useLibraryQuery(['tags.list'], { keepPreviousData: true }); - const onlineLocations = useOnlineLocations(); - - const createLocation = useLibraryMutation('locations.create'); - - return ( - <> -
- - - - } - > - {locations.data?.map((location) => { - const online = onlineLocations?.some((l) => arraysEqual(location.pub_id, l)); - - return ( - - ); - })} - {(locations.data?.length || 0) < 4 && ( - - )} - -
- {!!tags.data?.length && ( - - - - } - > -
- {tags.data?.slice(0, 6).map((tag, index) => ( - -
- {tag.name} - - ))} -
- - )} - - ); -} - -interface SidebarLocationProps { - location: Location; - online: boolean; -} - -function SidebarLocation({ location, online }: SidebarLocationProps) { - return ( -
- -
- -
-
- - {location.name} - -
- ); -} - -const Icon = ({ component: Icon, ...props }: any) => ( - -); - -// cute little helper to decrease code clutter -const macOnly = (platform: OperatingSystem | undefined, classnames: string) => - platform === 'macOS' ? classnames : ''; - -function WindowControls() { - const { platform } = usePlatform(); - const os = useOperatingSystem(); - - const showControls = window.location.search.includes('showControls'); - if (platform === 'tauri' || showControls) { - return ( -
- {/* We do not provide the onClick handlers for 'MacTrafficLights' because this is only used in demo mode */} - {showControls && } -
- ); - } - - return null; -} diff --git a/packages/interface/src/components/onboarding/OnboardingNodeSetup.tsx b/packages/interface/src/components/onboarding/OnboardingNodeSetup.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/interface/src/components/onboarding/OnboardingRoot.tsx b/packages/interface/src/components/onboarding/OnboardingRoot.tsx deleted file mode 100644 index e872e163d..000000000 --- a/packages/interface/src/components/onboarding/OnboardingRoot.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import BloomOne from '@sd/assets/images/bloom-one.png'; -import clsx from 'clsx'; -import { useEffect } from 'react'; -import { Navigate, Outlet, RouteObject, useNavigate } from 'react-router'; -import { getOnboardingStore } from '@sd/client'; -import { tw } from '@sd/ui'; -import DragRegion from '~/components/layout/DragRegion'; -import { useOperatingSystem } from '../../hooks/useOperatingSystem'; -import OnboardingCreatingLibrary from './OnboardingCreatingLibrary'; -import OnboardingMasterPassword from './OnboardingMasterPassword'; -import OnboardingNewLibrary from './OnboardingNewLibrary'; -import OnboardingPrivacy from './OnboardingPrivacy'; -import OnboardingProgress from './OnboardingProgress'; -import OnboardingStart from './OnboardingStart'; - -export const ONBOARDING_ROUTES: RouteObject[] = [ - { - index: true, - element: - }, - { - element: , - path: 'start' - }, - { - element: , - path: 'new-library' - }, - { - element: , - path: 'master-password' - }, - { - element: , - path: 'privacy' - }, - { - element: , - path: 'creating-library' - } -]; - -export const OnboardingContainer = tw.div`flex flex-col items-center`; -export const OnboardingTitle = tw.h2`mb-2 text-3xl font-bold`; -export const OnboardingDescription = tw.p`max-w-xl text-center text-ink-dull`; -export const OnboardingImg = tw.img`w-20 h-20 mb-2`; - -export default function OnboardingRoot() { - const os = useOperatingSystem(); - const navigate = useNavigate(); - const ob_store = getOnboardingStore(); - - useEffect(() => { - // This is neat because restores the last active screen, but only if it is not the starting screen - // Ignoring if people navigate back to the start if progress has been made - if (ob_store.unlockedScreens.length > 1) { - navigate(`/onboarding/${ob_store.lastActiveScreen}`); - } - }, []); - - return ( -
- - -
-
- -
- -
-
-

© 2022 Spacedrive Technology Inc.

-
-
-
- - {/* */} -
-
-
- ); -} - -const macOnly = (platform: string | undefined, classnames: string) => - platform === 'macOS' ? classnames : ''; diff --git a/packages/interface/src/components/onboarding/helpers/screens.ts b/packages/interface/src/components/onboarding/helpers/screens.ts deleted file mode 100644 index 00a9c2f19..000000000 --- a/packages/interface/src/components/onboarding/helpers/screens.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useLocation } from 'react-router-dom'; - -export const ONBOARDING_ROUTE_PREFIX_NAME = 'onboarding'; - -export const useCurrentOnboardingScreenKey = (): string | null => { - const { pathname } = useLocation(); - - if (pathname.startsWith(`/${ONBOARDING_ROUTE_PREFIX_NAME}/`)) { - return pathname.split('/')[2] || null; - } - - return null; -}; diff --git a/packages/interface/src/components/primitive/Listbox.tsx b/packages/interface/src/components/primitive/Listbox.tsx deleted file mode 100644 index 1080db11f..000000000 --- a/packages/interface/src/components/primitive/Listbox.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Listbox as ListboxPrimitive } from '@headlessui/react'; -import clsx from 'clsx'; -import { Check, Sun } from 'phosphor-react'; -import { useEffect, useState } from 'react'; - -interface ListboxOption { - option: string; - description?: string; - key: string; -} - -export default function Listbox(props: { options: ListboxOption[]; className?: string }) { - const [selected, setSelected] = useState(props.options[0]); - - useEffect(() => { - if (!selected) { - setSelected(props.options[0]); - } - }, [props.options, selected]); - - return ( - <> - -
- - {selected?.option ? ( - {selected?.option} - ) : ( - Nothing selected... - )} - - - - - - - {props.options.map((option, index) => ( - - `relative m-1 cursor-default select-none rounded py-2 pl-8 pr-4 focus:outline-none dark:text-white ${ - active ? 'text-accent bg-accent' : 'text-gray-900 dark:hover:bg-gray-600/20' - }` - } - value={option} - > - {({ selected }) => ( - <> - - {option.option} - {option.description && ( - - {option.description} - - )} - - - {selected ? ( - - - ) : null} - - )} - - ))} - -
-
- - ); -} diff --git a/packages/interface/src/components/primitive/Tag.tsx b/packages/interface/src/components/primitive/Tag.tsx deleted file mode 100644 index d8c003db8..000000000 --- a/packages/interface/src/components/primitive/Tag.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import clsx from 'clsx'; -import { PropsWithChildren, ReactNode } from 'react'; -import { DefaultProps } from './types'; - -export interface TagProps extends DefaultProps { - color: 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple' | 'pink'; -} - -export function Tag(props: PropsWithChildren) { - return ( -
- {props.children} -
- ); -} diff --git a/packages/interface/src/components/primitive/index.tsx b/packages/interface/src/components/primitive/index.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/interface/src/components/primitive/types.ts b/packages/interface/src/components/primitive/types.ts deleted file mode 100644 index 217b4bdcb..000000000 --- a/packages/interface/src/components/primitive/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { DetailedHTMLProps, HTMLAttributes } from 'react'; - -export interface DefaultProps - extends DetailedHTMLProps, E> { - className?: string; -} diff --git a/packages/interface/src/components/settings/SettingsContainer.tsx b/packages/interface/src/components/settings/SettingsContainer.tsx deleted file mode 100644 index 30e99022d..000000000 --- a/packages/interface/src/components/settings/SettingsContainer.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import clsx from 'clsx'; -import { PropsWithChildren } from 'react'; -import { useOperatingSystem } from '../../hooks/useOperatingSystem'; - -export const SettingsContainer = ({ children }: PropsWithChildren) => { - const os = useOperatingSystem(); - - return ( - <> - {os !== 'browser' ? ( -
- ) : ( -
- )} -
-
- {children} -
-
-
- - ); -}; diff --git a/packages/interface/src/components/settings/SettingsHeader.tsx b/packages/interface/src/components/settings/SettingsHeader.tsx deleted file mode 100644 index f88931d08..000000000 --- a/packages/interface/src/components/settings/SettingsHeader.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import clsx from 'clsx'; -import { PropsWithChildren, ReactNode } from 'react'; - -interface SettingsHeaderProps extends PropsWithChildren { - title: string; - description: string | ReactNode; - rightArea?: ReactNode; -} - -export const SettingsHeader: React.FC = (props) => { - return ( -
- {props.children} -
-

{props.title}

-

{props.description}

-
- {props.rightArea} -
-
- ); -}; - -export const SettingsIcon = ({ component: Icon, ...props }: any) => ( - -); - -export function SettingsHeading({ - children, - className -}: PropsWithChildren<{ className?: string }>) { - return ( -
- {children} -
- ); -} diff --git a/packages/interface/src/components/settings/SettingsSidebar.tsx b/packages/interface/src/components/settings/SettingsSidebar.tsx deleted file mode 100644 index 26483d8d8..000000000 --- a/packages/interface/src/components/settings/SettingsSidebar.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { - Books, - FlyingSaucer, - GearSix, - Graph, - HardDrive, - Heart, - Key, - KeyReturn, - PaintBrush, - PuzzlePiece, - Receipt, - ShareNetwork, - ShieldCheck, - TagSimple -} from 'phosphor-react'; -import { useOperatingSystem } from '../../hooks/useOperatingSystem'; -import { SidebarLink } from '../layout/Sidebar'; -import { SettingsHeading, SettingsIcon } from './SettingsHeader'; - -export const SettingsSidebar = () => { - const os = useOperatingSystem(); - return ( -
- {os !== 'browser' ? ( -
- ) : ( -
- )} -
- Client - - - General - - - - Libraries - - - - Privacy - - - - Appearance - - - - Keybinds - - - - Extensions - - - Library - - - General - - - - Nodes - - - - Locations - - - - Tags - - - - Keys - - Resources - - - About - - - - Changelog - - - - Dependencies - - - - Support - -
-
- ); -}; diff --git a/packages/interface/src/components/settings/SettingsSubHeader.tsx b/packages/interface/src/components/settings/SettingsSubHeader.tsx deleted file mode 100644 index cf07f50c2..000000000 --- a/packages/interface/src/components/settings/SettingsSubHeader.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ReactNode } from 'react'; - -export interface SettingsSubHeaderProps { - title: string; - rightArea?: ReactNode; -} - -export const SettingsSubHeader: React.FC = (props) => { - return ( -
-
-

{props.title}

-
- {props.rightArea} -
- ); -}; diff --git a/packages/interface/src/components/settings/SettingsSubPage.tsx b/packages/interface/src/components/settings/SettingsSubPage.tsx deleted file mode 100644 index 8849fa2bc..000000000 --- a/packages/interface/src/components/settings/SettingsSubPage.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { ReactComponent as CaretDown } from '@sd/assets/svgs/caret.svg'; -import { PropsWithChildren } from 'react'; -import { useNavigate } from 'react-router'; -import { Button, tw } from '@sd/ui'; -import DragRegion from '~/components/layout/DragRegion'; -import { Divider } from '../explorer/inspector/Divider'; - -interface Props extends PropsWithChildren { - title: string; - topRight?: React.ReactNode; -} - -const PageOuter = tw.div`flex h-screen flex-col m-3`; -const Page = tw.div`flex-1 w-full border rounded-md shadow-md shadow-app-shade/30 border-app-box bg-app-box/20`; -const PageInner = tw.div`flex flex-col max-w-4xl w-full h-screen py-6`; -const HeaderArea = tw.div`flex flex-row px-8 items-center space-x-4 mb-2`; -const ContentContainer = tw.div`px-8 pt-5 -mt-1 space-y-6 custom-scroll page-scroll`; - -export const SettingsSubPage = ({ children, title, topRight }: Props) => { - const navigate = useNavigate(); - - return ( - - - - - - -

{title}

- {topRight} -
-
- -
- {children} -
-
-
- ); -}; diff --git a/packages/interface/src/components/transitions/SlideUp.tsx b/packages/interface/src/components/transitions/SlideUp.tsx deleted file mode 100644 index d1b993171..000000000 --- a/packages/interface/src/components/transitions/SlideUp.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Transition } from '@headlessui/react'; -import { PropsWithChildren } from 'react'; - -export default function SlideUp(props: PropsWithChildren) { - return ( - - {props.children} - - ); -} diff --git a/packages/interface/src/constants/demo-data.json b/packages/interface/src/constants/demo-data.json deleted file mode 100644 index f2afca612..000000000 --- a/packages/interface/src/constants/demo-data.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "node_state": { - "node_pub_id": "f4deae23-e578-456e-8897-9ff59444c08b", - "node_id": 1, - "node_name": "Jamie's MacBook Pro", - "data_path": "/Users/jamie/Library/Application Support/spacedrive", - "tcp_port": 0, - "libraries": [ - { - "library_uuid": "9816efb4-5f2d-4c84-8674-818767bb1184", - "library_id": 0, - "library_path": "/Users/jamie/Library/Application Support/spacedrive/library.db", - "offline": false - } - ], - "current_library_uuid": "9816efb4-5f2d-4c84-8674-818767bb1184" - }, - "libraries": [ - { - "id": 1, - "pub_id": "9816efb4-5f2d-4c84-8674-818767bb1184", - "date_created": "2020-04-01T00:00:00.000Z", - "is_primary": true - } - ], - "nodes": [ - { - "id": 1, - "pub_id": "f4deae23-e578-456e-8897-9ff59444c08b", - "name": "Jamie's MacBook Pro", - "platform": "macos", - "version": "0.0.1", - "date_created": "2020-04-01T00:00:00.000Z", - "last_seen": "2020-04-01T00:00:00.000Z", - "timezone": "America/Los_Angeles" - }, - { - "id": 1, - "pub_id": "8fec7649-08d8-4828-baa3-e1078d9f15ee", - "name": "Jamie's iPhone 12", - "platform": "ios", - "version": "0.0.1", - "date_created": "2020-04-01T00:00:00.000Z", - "last_seen": "2020-04-01T00:00:00.000Z", - "timezone": "America/Los_Angeles" - }, - { - "id": 1, - "pub_id": "67f66377-d38e-4fb7-961a-c1193a58b458", - "name": "Spacedrive Server", - "platform": "server", - "version": "0.0.1", - "date_created": "2020-04-01T00:00:00.000Z", - "last_seen": "2020-04-01T00:00:00.000Z", - "timezone": "America/Los_Angeles" - } - ] -} diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts deleted file mode 100644 index 3ca16a2fc..000000000 --- a/packages/interface/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import SpacedriveInterface from './App'; - -export { KeybindEvent } from './util/keybind'; -export * from './util/Platform'; - -export default SpacedriveInterface; - -export { ErrorPage } from './ErrorFallback'; diff --git a/packages/interface/src/screens/Media.tsx b/packages/interface/src/screens/Media.tsx deleted file mode 100644 index 1cccda31a..000000000 --- a/packages/interface/src/screens/Media.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ScreenHeading } from '@sd/ui'; -import { ScreenContainer } from './_Layout'; - -export default function MediaScreen() { - return ( - - Media - - ); -} diff --git a/packages/interface/src/screens/People.tsx b/packages/interface/src/screens/People.tsx deleted file mode 100644 index db0e8df8a..000000000 --- a/packages/interface/src/screens/People.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ScreenHeading } from '@sd/ui'; -import { ScreenContainer } from './_Layout'; - -export default function PeopleScreen() { - return ( - - People - - ); -} diff --git a/packages/interface/src/screens/Spaces.tsx b/packages/interface/src/screens/Spaces.tsx deleted file mode 100644 index c3b414716..000000000 --- a/packages/interface/src/screens/Spaces.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ScreenHeading } from '@sd/ui'; -import { ScreenContainer } from './_Layout'; - -export default function SpacesScreen() { - return ( - - Spaces - - ); -} diff --git a/packages/interface/src/screens/TagExplorer.tsx b/packages/interface/src/screens/TagExplorer.tsx deleted file mode 100644 index 0cf306544..000000000 --- a/packages/interface/src/screens/TagExplorer.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useParams } from 'react-router-dom'; -import { useLibraryContext, useLibraryQuery } from '@sd/client'; -import Explorer from '~/components/explorer/Explorer'; - -export default function TagExplorer() { - const { id } = useParams(); - const { library } = useLibraryContext(); - - const explorerData = useLibraryQuery(['tags.getExplorerData', Number(id)]); - - return ( -
- {library!.uuid && id != undefined && explorerData.data && ( - - )} -
- ); -} diff --git a/packages/interface/src/screens/_Layout.tsx b/packages/interface/src/screens/_Layout.tsx deleted file mode 100644 index d8bd0e513..000000000 --- a/packages/interface/src/screens/_Layout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import clsx from 'clsx'; -import { PropsWithChildren, ReactNode, createContext } from 'react'; -import DragRegion from '~/components/layout/DragRegion'; - -export function ScreenContainer( - props: PropsWithChildren & { className?: string; dragRegionChildren?: ReactNode } -) { - return ( -
- {props.dragRegionChildren} -
{props.children}
-
- ); -} diff --git a/packages/interface/src/screens/index.tsx b/packages/interface/src/screens/index.tsx deleted file mode 100644 index bb15c1617..000000000 --- a/packages/interface/src/screens/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { RouteObject } from 'react-router-dom'; -import { lazyEl } from '~/util'; -import settingsScreens from './settings'; - -const screens: RouteObject[] = [ - { - path: 'overview', - element: lazyEl(() => import('./Overview')) - }, - { path: 'people', element: lazyEl(() => import('./People')) }, - { path: 'media', element: lazyEl(() => import('./Media')) }, - { path: 'spaces', element: lazyEl(() => import('./Spaces')) }, - { path: 'debug', element: lazyEl(() => import('./Debug')) }, - { path: 'spacedrop', element: lazyEl(() => import('./Spacedrop')) }, - { path: 'location/:id', element: lazyEl(() => import('./LocationExplorer')) }, - { path: 'tag/:id', element: lazyEl(() => import('./TagExplorer')) }, - { - path: 'settings', - element: lazyEl(() => import('./settings/_Layout')), - children: settingsScreens - }, - { path: '*', element: lazyEl(() => import('./NotFound')) } -]; - -export default screens; diff --git a/packages/interface/src/screens/settings/SettingsSubPage.tsx b/packages/interface/src/screens/settings/SettingsSubPage.tsx deleted file mode 100644 index 10c633406..000000000 --- a/packages/interface/src/screens/settings/SettingsSubPage.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Suspense } from 'react'; -import { Outlet } from 'react-router'; - -export default function SettingsSubPageScreen() { - return ( -
-
- - - -
-
- ); -} diff --git a/packages/interface/src/screens/settings/_Layout.tsx b/packages/interface/src/screens/settings/_Layout.tsx deleted file mode 100644 index 6d7667500..000000000 --- a/packages/interface/src/screens/settings/_Layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Suspense } from 'react'; -import { Outlet } from 'react-router'; -import { SettingsSidebar } from '~/components/settings/SettingsSidebar'; - -export default function SettingsScreenContainer() { - return ( -
- -
- - - -
-
- ); -} diff --git a/packages/interface/src/screens/settings/client/AppearanceSettings.tsx b/packages/interface/src/screens/settings/client/AppearanceSettings.tsx deleted file mode 100644 index e6382776c..000000000 --- a/packages/interface/src/screens/settings/client/AppearanceSettings.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useEffect, useState } from 'react'; -import { forms } from '@sd/ui'; -import { InputContainer } from '~/components/primitive/InputContainer'; -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; - -const { Form, Input, Switch, useZodForm, z } = forms; - -const schema = z.object({ - uiAnimations: z.boolean(), - syncThemeWithSystem: z.boolean(), - blurEffects: z.boolean() -}); - -export default function AppearanceSettings() { - const form = useZodForm({ - schema - }); - - const onSubmit = form.handleSubmit(async (data) => { - console.log({ data }); - }); - - useEffect(() => { - const subscription = form.watch(() => onSubmit()); - return () => subscription.unsubscribe(); - }, [form, onSubmit]); - - return ( -
- - - - - - - - - - - - - -
- ); -} diff --git a/packages/interface/src/screens/settings/index.tsx b/packages/interface/src/screens/settings/index.tsx deleted file mode 100644 index e7fe16de9..000000000 --- a/packages/interface/src/screens/settings/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { RouteObject } from 'react-router-dom'; -import { lazyEl } from '~/util'; - -const screens: RouteObject[] = [ - { path: 'general', element: lazyEl(() => import('./client/GeneralSettings')) }, - { path: 'appearance', element: lazyEl(() => import('./client/AppearanceSettings')) }, - { path: 'keybindings', element: lazyEl(() => import('./client/KeybindingSettings')) }, - { path: 'extensions', element: lazyEl(() => import('./client/ExtensionsSettings')) }, - { path: 'p2p', element: lazyEl(() => import('./node/P2PSettings')) }, - { path: 'contacts', element: lazyEl(() => import('./library/ContactsSettings')) }, - { path: 'experimental', element: lazyEl(() => import('./node/ExperimentalSettings')) }, - { path: 'keys', element: lazyEl(() => import('./library/KeysSetting')) }, - { path: 'libraries', element: lazyEl(() => import('./node/LibrariesSettings')) }, - { path: 'security', element: lazyEl(() => import('./library/SecuritySettings')) }, - { path: 'locations', element: lazyEl(() => import('./library/LocationsSettings')) }, - { path: 'sharing', element: lazyEl(() => import('./library/SharingSettings')) }, - { path: 'sync', element: lazyEl(() => import('./library/SyncSettings')) }, - { path: 'tags', element: lazyEl(() => import('./library/TagsSettings')) }, - { path: 'library', element: lazyEl(() => import('./library/LibraryGeneralSettings')) }, - { path: 'tags', element: lazyEl(() => import('./library/TagsSettings')) }, - { path: 'nodes', element: lazyEl(() => import('./library/NodesSettings')) }, - { path: 'privacy', element: lazyEl(() => import('./client/PrivacySettings')) }, - { path: 'about', element: lazyEl(() => import('./info/AboutSpacedrive')) }, - { path: 'changelog', element: lazyEl(() => import('./info/Changelog')) }, - { path: 'dependencies', element: lazyEl(() => import('./info/Dependencies')) }, - { path: 'support', element: lazyEl(() => import('./info/Support')) }, - { - path: 'locations', - element: lazyEl(() => import('./SettingsSubPage')), - children: [ - { index: true, element: lazyEl(() => import('./library/LocationsSettings')) }, - { path: ':id', element: lazyEl(() => import('./library/location/EditLocation')) } - ] - } -]; - -export default screens; diff --git a/packages/interface/src/screens/settings/info/Changelog.tsx b/packages/interface/src/screens/settings/info/Changelog.tsx deleted file mode 100644 index 15c3140d7..000000000 --- a/packages/interface/src/screens/settings/info/Changelog.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { SettingsContainer } from '../../../components/settings/SettingsContainer'; -import { SettingsHeader } from '../../../components/settings/SettingsHeader'; - -export default function Changelog() { - return ( - - - - ); -} diff --git a/packages/interface/src/screens/settings/info/Support.tsx b/packages/interface/src/screens/settings/info/Support.tsx deleted file mode 100644 index 93811ade9..000000000 --- a/packages/interface/src/screens/settings/info/Support.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { SettingsContainer } from '../../../components/settings/SettingsContainer'; -import { SettingsHeader } from '../../../components/settings/SettingsHeader'; - -export default function Support() { - return ( - - - - ); -} diff --git a/packages/interface/src/screens/settings/library/BackupsSettings.tsx b/packages/interface/src/screens/settings/library/BackupsSettings.tsx deleted file mode 100644 index e4575c69f..000000000 --- a/packages/interface/src/screens/settings/library/BackupsSettings.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; - -export default function NodesSettings() { - return ( - - - - ); -} diff --git a/packages/interface/src/screens/settings/library/ContactsSettings.tsx b/packages/interface/src/screens/settings/library/ContactsSettings.tsx deleted file mode 100644 index 56203344f..000000000 --- a/packages/interface/src/screens/settings/library/ContactsSettings.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; - -export default function ContactsSettings() { - return ( - - - - ); -} diff --git a/packages/interface/src/screens/settings/library/SecuritySettings.tsx b/packages/interface/src/screens/settings/library/SecuritySettings.tsx deleted file mode 100644 index 8ab9377b1..000000000 --- a/packages/interface/src/screens/settings/library/SecuritySettings.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; - -export default function SecuritySettings() { - return ( - - - - ); -} diff --git a/packages/interface/src/screens/settings/library/SharingSettings.tsx b/packages/interface/src/screens/settings/library/SharingSettings.tsx deleted file mode 100644 index 237cd3502..000000000 --- a/packages/interface/src/screens/settings/library/SharingSettings.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; - -export default function SharingSettings() { - return ( - - - - ); -} diff --git a/packages/interface/src/screens/settings/library/SyncSettings.tsx b/packages/interface/src/screens/settings/library/SyncSettings.tsx deleted file mode 100644 index 71360396d..000000000 --- a/packages/interface/src/screens/settings/library/SyncSettings.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; - -export default function SyncSettings() { - return ( - - - - ); -} diff --git a/packages/interface/src/screens/settings/library/TagsSettings.tsx b/packages/interface/src/screens/settings/library/TagsSettings.tsx deleted file mode 100644 index 9c1e2c66b..000000000 --- a/packages/interface/src/screens/settings/library/TagsSettings.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import clsx from 'clsx'; -import { Trash } from 'phosphor-react'; -import { useCallback, useEffect, useState } from 'react'; -import { useDebounce } from 'rooks'; -import { Tag, useLibraryMutation, useLibraryQuery } from '@sd/client'; -import { Button, Card, Dialog, Switch, UseDialogProps, dialogManager, useDialog } from '@sd/ui'; -import { Form, Input, useZodForm, z } from '@sd/ui/src/forms'; -import { InputContainer } from '~/components/primitive/InputContainer'; -import { PopoverPicker } from '~/components/primitive/PopoverPicker'; -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; -import { Tooltip } from '~/components/tooltip/Tooltip'; - -export default function TagsSettings() { - const tags = useLibraryQuery(['tags.list']); - - const [selectedTag, setSelectedTag] = useState(tags.data?.[0] ?? null); - - const updateTag = useLibraryMutation('tags.update'); - - const updateForm = useZodForm({ - schema: z.object({ - id: z.number(), - name: z.string().nullable(), - color: z.string().nullable() - }), - defaultValues: selectedTag ?? undefined - }); - - const submitTagUpdate = updateForm.handleSubmit((data) => updateTag.mutateAsync(data)); - // eslint-disable-next-line react-hooks/exhaustive-deps - const autoUpdateTag = useCallback(useDebounce(submitTagUpdate, 500), [submitTagUpdate]); - - const setTag = useCallback( - (tag: Tag | null) => { - if (tag) updateForm.reset(tag); - setSelectedTag(tag); - }, - [setSelectedTag, updateForm] - ); - - useEffect(() => { - const subscription = updateForm.watch(() => autoUpdateTag()); - return () => subscription.unsubscribe(); - }, [updateForm, autoUpdateTag]); - - return ( - - - -
- } - /> - -
- {tags.data?.map((tag) => ( -
setTag(tag.id === selectedTag?.id ? null : tag)} - key={tag.id} - className={clsx( - 'flex items-center rounded px-1.5 py-0.5', - selectedTag?.id === tag.id && 'ring' - )} - style={{ backgroundColor: tag.color + 'CC' }} - > - {tag.name} -
- ))} -
-
- {selectedTag ? ( -
-
-
- - Color - -
- - -
-
-
- - Name - - -
-
- -
- - - - - ) : ( -
No Tag Selected
- )} - - ); -} - -function CreateTagDialog(props: UseDialogProps) { - const dialog = useDialog(props); - - const form = useZodForm({ - schema: z.object({ - name: z.string(), - color: z.string() - }), - defaultValues: { - color: '#A717D9' - } - }); - - const createTag = useLibraryMutation('tags.create', { - onError: (e) => { - console.error('error', e); - } - }); - - return ( - createTag.mutateAsync(data))} - title="Create New Tag" - description="Choose a name and color." - ctaLabel="Create" - > -
- - -
-
- ); -} - -interface DeleteTagDialogProps extends UseDialogProps { - tagId: number; - onSuccess: () => void; -} - -function DeleteTagDialog(props: DeleteTagDialogProps) { - const dialog = useDialog(props); - - const form = useZodForm({ schema: z.object({}) }); - - const deleteTag = useLibraryMutation('tags.delete', { - onSuccess: props.onSuccess - }); - - return ( - deleteTag.mutateAsync(props.tagId))} - title="Delete Tag" - description="Are you sure you want to delete this tag? This cannot be undone and tagged files will be unlinked." - ctaDanger - ctaLabel="Delete" - /> - ); -} diff --git a/packages/interface/src/screens/settings/node/ExperimentalSettings.tsx b/packages/interface/src/screens/settings/node/ExperimentalSettings.tsx deleted file mode 100644 index b72014879..000000000 --- a/packages/interface/src/screens/settings/node/ExperimentalSettings.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Switch } from '@sd/ui'; -import { useNodeStore } from '~/components/device/Stores'; -import { InputContainer } from '~/components/primitive/InputContainer'; -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; - -export default function ExperimentalSettings() { - const { isExperimental, setIsExperimental } = useNodeStore(); - - return ( - - {/* */} - - -
- { - setIsExperimental(!isExperimental); - }} - /> -
-
-
- ); -} diff --git a/packages/interface/src/screens/settings/node/LibrariesSettings.tsx b/packages/interface/src/screens/settings/node/LibrariesSettings.tsx deleted file mode 100644 index 5559f3cd5..000000000 --- a/packages/interface/src/screens/settings/node/LibrariesSettings.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Database, DotsSixVertical, Pencil, Trash } from 'phosphor-react'; -import { useBridgeQuery, useLibraryContext } from '@sd/client'; -import { LibraryConfigWrapped } from '@sd/client'; -import { Button, ButtonLink, Card, dialogManager, tw } from '@sd/ui'; -import CreateLibraryDialog from '~/components/dialog/CreateLibraryDialog'; -import DeleteLibraryDialog from '~/components/dialog/DeleteLibraryDialog'; -import { SettingsContainer } from '~/components/settings/SettingsContainer'; -import { SettingsHeader } from '~/components/settings/SettingsHeader'; -import { Tooltip } from '~/components/tooltip/Tooltip'; - -const Pill = tw.span`px-1.5 ml-2 py-[2px] rounded text-xs font-medium bg-accent`; - -function LibraryListItem(props: { library: LibraryConfigWrapped; current: boolean }) { - return ( - - -
-

- {props.library.config.name} - {props.current && Current} -

-

{props.library.uuid}

-
-
- - - - - - - -
-
- ); -} - -export default function LibrarySettings() { - const libraries = useBridgeQuery(['library.list']); - - const { library } = useLibraryContext(); - - return ( - - - -
- } - /> - -
- {libraries.data - ?.sort((a, b) => { - if (a.uuid === library.uuid) return -1; - if (b.uuid === library.uuid) return 1; - return 0; - }) - .map((library) => ( - - ))} -
- - ); -} diff --git a/packages/interface/src/util/dialog.tsx b/packages/interface/src/util/dialog.tsx deleted file mode 100644 index a9d92ce77..000000000 --- a/packages/interface/src/util/dialog.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { dialogManager } from '@sd/ui'; -import { AlertDialog, AlertDialogProps } from '~/components/dialog/AlertDialog'; - -export function showAlertDialog(props: Omit) { - dialogManager.create((dp) => ); -} diff --git a/packages/ui/src/Button.tsx b/packages/ui/src/Button.tsx index dfbeaf369..1b3bae47d 100644 --- a/packages/ui/src/Button.tsx +++ b/packages/ui/src/Button.tsx @@ -34,7 +34,7 @@ const styles = cva( }, size: { icon: '!p-1', - lg: 'py-1.5 px-3 text-md font-medium', + lg: 'text-md py-1.5 px-3 font-medium', md: 'py-1.5 px-2.5 text-sm font-medium', sm: 'py-1 px-2 text-sm font-medium' }, diff --git a/packages/ui/src/ContextMenu.tsx b/packages/ui/src/ContextMenu.tsx index daa8c8e5e..59dd17ce3 100644 --- a/packages/ui/src/ContextMenu.tsx +++ b/packages/ui/src/ContextMenu.tsx @@ -10,19 +10,14 @@ interface Props extends RadixCM.MenuContentProps { const MENU_CLASSES = ` flex flex-col z-50 - min-w-[8rem] px-1 py-0.5 + min-w-[8rem] px-1 py-0.5 my-2 text-left text-sm text-menu-ink bg-menu cool-shadow border border-menu-line select-none cursor-default rounded-md `; -export const ContextMenu = ({ - trigger, - children, - className, - ...props -}: PropsWithChildren) => { +export const Root = ({ trigger, children, className, ...props }: PropsWithChildren) => { return ( {trigger} diff --git a/packages/ui/src/Dialog.tsx b/packages/ui/src/Dialog.tsx index f9be34e94..03df70eef 100644 --- a/packages/ui/src/Dialog.tsx +++ b/packages/ui/src/Dialog.tsx @@ -162,7 +162,7 @@ export function Dialog({
{ - await onSubmit(e); + await onSubmit?.(e); dialog.onSubmit?.(); setOpen(false); }} diff --git a/packages/ui/src/Divider.tsx b/packages/ui/src/Divider.tsx new file mode 100644 index 000000000..219f37ba2 --- /dev/null +++ b/packages/ui/src/Divider.tsx @@ -0,0 +1,3 @@ +import { tw } from './utils'; + +export const Divider = tw.div`bg-app-line/60 my-1 h-[1px] w-full`; diff --git a/packages/interface/src/components/icons/Folder.tsx b/packages/ui/src/Folder.tsx similarity index 100% rename from packages/interface/src/components/icons/Folder.tsx rename to packages/ui/src/Folder.tsx diff --git a/packages/ui/src/Input.tsx b/packages/ui/src/Input.tsx index b93347a2b..e7a57df5b 100644 --- a/packages/ui/src/Input.tsx +++ b/packages/ui/src/Input.tsx @@ -12,8 +12,9 @@ export type TextareaProps = InputBaseProps & React.ComponentProps<'textarea'>; const styles = cva( [ - 'px-3 text-sm rounded-md border leading-7', - 'outline-none shadow-sm focus:ring-2 transition-all' + 'w-full', + 'rounded-md border px-3 text-sm leading-7', + 'shadow-sm outline-none transition-all focus:ring-2' ], { variants: { @@ -24,8 +25,8 @@ const styles = cva( ] }, size: { - sm: 'text-sm py-0.5', - md: 'text-sm py-1' + sm: 'py-0.5 text-sm', + md: 'py-1 text-sm' } }, defaultVariants: { @@ -65,33 +66,28 @@ export function Label(props: PropsWithChildren<{ slug?: string }>) { ); } -interface PasswordShowHideInputProps extends InputProps { +interface PasswordInputProps extends InputProps { buttonClassnames?: string; } -export const PasswordShowHideInput = forwardRef( - ({ variant, size, className, ...props }, ref) => { - const [showPassword, setShowPassword] = useState(false); - const CurrentEyeIcon = showPassword ? EyeSlash : Eye; - return ( - - - - - ); - } -); +export const PasswordInput = forwardRef((props, ref) => { + const [showPassword, setShowPassword] = useState(false); + + const CurrentEyeIcon = showPassword ? EyeSlash : Eye; + + return ( +
+ + +
+ ); +}); diff --git a/packages/interface/src/components/key/PasswordMeter.tsx b/packages/ui/src/PasswordMeter.tsx similarity index 88% rename from packages/interface/src/components/key/PasswordMeter.tsx rename to packages/ui/src/PasswordMeter.tsx index 0a0af266a..20393f691 100644 --- a/packages/interface/src/components/key/PasswordMeter.tsx +++ b/packages/ui/src/PasswordMeter.tsx @@ -13,9 +13,13 @@ const options = { }; zxcvbnOptions.setOptions(options); -export default function PasswordMeterInner(props: { password: string }) { - const ratings = ['Poor', 'Weak', 'Good', 'Strong', 'Perfect']; +const ratings = ['Poor', 'Weak', 'Good', 'Strong', 'Perfect']; +export interface PasswordMeterProps { + password: string; +} + +export const PasswordMeter = (props: PasswordMeterProps) => { const zx = zxcvbn(props.password); const widthCalcStyle = { @@ -54,4 +58,4 @@ export default function PasswordMeterInner(props: { password: string }) {
); -} +}; diff --git a/packages/interface/src/components/primitive/ProgressBar.tsx b/packages/ui/src/ProgressBar.tsx similarity index 78% rename from packages/interface/src/components/primitive/ProgressBar.tsx rename to packages/ui/src/ProgressBar.tsx index 7765eb784..f2b127f2a 100644 --- a/packages/interface/src/components/primitive/ProgressBar.tsx +++ b/packages/ui/src/ProgressBar.tsx @@ -1,12 +1,14 @@ import * as ProgressPrimitive from '@radix-ui/react-progress'; +import { memo } from 'react'; -interface Props { +export interface ProgressBarProps { value: number; total: number; } -const ProgressBar = (props: Props) => { +export const ProgressBar = memo((props: ProgressBarProps) => { const percentage = Math.round((props.value / props.total) * 100); + return ( { /> ); -}; - -export default ProgressBar; +}); diff --git a/packages/interface/src/components/primitive/Shortcut.tsx b/packages/ui/src/Shortcut.tsx similarity index 67% rename from packages/interface/src/components/primitive/Shortcut.tsx rename to packages/ui/src/Shortcut.tsx index 38427acd9..9f8a46dae 100644 --- a/packages/interface/src/components/primitive/Shortcut.tsx +++ b/packages/ui/src/Shortcut.tsx @@ -1,11 +1,11 @@ import clsx from 'clsx'; -import { DefaultProps } from './types'; +import { ComponentProps } from 'react'; -export interface ShortcutProps extends DefaultProps { +export interface ShortcutProps extends ComponentProps<'div'> { chars: string; } -export const Shortcut: React.FC = (props) => { +export const Shortcut = (props: ShortcutProps) => { const { className, chars, ...rest } = props; return ( diff --git a/packages/interface/src/components/primitive/Slider.tsx b/packages/ui/src/Slider.tsx similarity index 88% rename from packages/interface/src/components/primitive/Slider.tsx rename to packages/ui/src/Slider.tsx index 667fb997d..130165a7f 100644 --- a/packages/interface/src/components/primitive/Slider.tsx +++ b/packages/ui/src/Slider.tsx @@ -1,7 +1,7 @@ import * as SliderPrimitive from '@radix-ui/react-slider'; import clsx from 'clsx'; -const Slider = (props: SliderPrimitive.SliderProps) => ( +export const Slider = (props: SliderPrimitive.SliderProps) => ( ( /> ); - -export default Slider; diff --git a/packages/interface/src/components/tooltip/Tooltip.tsx b/packages/ui/src/Tooltip.tsx similarity index 100% rename from packages/interface/src/components/tooltip/Tooltip.tsx rename to packages/ui/src/Tooltip.tsx diff --git a/packages/ui/src/forms/Form.tsx b/packages/ui/src/forms/Form.tsx index d7ef14b75..2daa8d523 100644 --- a/packages/ui/src/forms/Form.tsx +++ b/packages/ui/src/forms/Form.tsx @@ -12,7 +12,7 @@ import { z } from 'zod'; export interface FormProps extends Omit, 'onSubmit'> { form: UseFormReturn; - onSubmit: ReturnType>; + onSubmit?: ReturnType>; } export const Form = ({ @@ -21,21 +21,18 @@ export const Form = ({ children, ...props }: FormProps) => { - const { className, ...otherProps } = props; return ( { e.stopPropagation(); - return onSubmit(e); + return onSubmit?.(e); }} - {...otherProps} + {...props} > {/*
passes the form's 'disabled' state to all of its elements, allowing us to handle disabled style variants with just css */} -
- {children} -
+
{children}
); @@ -43,13 +40,18 @@ export const Form = ({ interface UseZodFormProps extends Exclude>, 'resolver'> { - schema: S; + schema?: S; } -export const useZodForm = ({ schema, ...formProps }: UseZodFormProps) => - useForm({ +export const useZodForm = >>( + props?: UseZodFormProps +) => { + const { schema, ...formProps } = props ?? {}; + + return useForm({ ...formProps, - resolver: zodResolver(schema) + resolver: zodResolver(schema || z.object({})) }); +}; export { z } from 'zod'; diff --git a/packages/ui/src/forms/Input.tsx b/packages/ui/src/forms/Input.tsx index 6099b1bbc..868889ef8 100644 --- a/packages/ui/src/forms/Input.tsx +++ b/packages/ui/src/forms/Input.tsx @@ -16,12 +16,12 @@ export const Input = forwardRef((props, ref) => { ); }); -export const PasswordShowHideInput = forwardRef((props, ref) => { +export const PasswordInput = forwardRef((props, ref) => { const { formFieldProps, childProps } = useFormField(props); return ( - + ); }); diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index b9d9dbef5..0f3188969 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -15,3 +15,10 @@ export * as RadioGroup from './RadioGroup'; export * from './Typography'; export * as forms from './forms'; export * from './utils'; +export * from './Tooltip'; +export * from './Slider'; +export * from './Divider'; +export * from './PasswordMeter'; +export * from './Shortcut'; +export * from './ProgressBar'; +export * from './Folder'; diff --git a/packages/ui/style/tailwind.js b/packages/ui/style/tailwind.js index b7c23d333..9d8b5b7d0 100644 --- a/packages/ui/style/tailwind.js +++ b/packages/ui/style/tailwind.js @@ -12,6 +12,7 @@ module.exports = function (app, options) { let config = { content: [ !options?.ignorePackages && '../../packages/*/src/**/*.{ts,tsx,html}', + '../../interface/**/*.{ts,tsx,html}', app ? `../../apps/${app}/src/**/*.{ts,tsx,html}` : `./src/**/*.{ts,tsx,html}` ], darkMode: app == 'landing' ? 'class' : 'media', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fa5c58ec..c0c77727b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: '@rspc/client': 0.0.0-main-7c0a67c1 '@rspc/tauri': 0.0.0-main-7c0a67c1_@tauri-apps+api@1.2.0 '@sd/client': link:../../packages/client - '@sd/interface': link:../../packages/interface + '@sd/interface': link:../../interface '@sd/ui': link:../../packages/ui '@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y '@tauri-apps/api': 1.2.0 @@ -318,7 +318,7 @@ importers: '@fontsource/inter': 4.5.15 '@rspc/client': 0.0.0-main-7c0a67c1 '@sd/client': link:../../packages/client - '@sd/interface': link:../../packages/interface + '@sd/interface': link:../../interface '@tanstack/react-query': 4.22.0_biqbaboplfbrettd7655fr4n2y react: 18.2.0 react-dom: 18.2.0_react@18.2.0 @@ -368,53 +368,7 @@ importers: docs: specifiers: {} - packages/assets: - specifiers: {} - - packages/client: - specifiers: - '@rspc/client': ^0.0.0-main-7c0a67c1 - '@rspc/react': ^0.0.0-main-7c0a67c1 - '@sd/config': workspace:* - '@tanstack/react-query': ^4.12.0 - '@types/react': ^18.0.21 - scripts: '*' - tsconfig: '*' - typescript: ^4.8.4 - valtio: ^1.7.4 - dependencies: - '@rspc/client': 0.0.0-main-7c0a67c1 - '@rspc/react': 0.0.0-main-7c0a67c1_tqsbl3x6ixew47ctvte2nz2yki - '@sd/config': link:../config - '@tanstack/react-query': 4.22.0 - valtio: 1.9.0 - devDependencies: - '@types/react': 18.0.27 - scripts: 0.1.0 - tsconfig: 7.0.0 - typescript: 4.9.4 - - packages/config: - specifiers: - '@typescript-eslint/eslint-plugin': ^5.51.0 - '@typescript-eslint/parser': ^5.51.0 - eslint: ^8.33.0 - eslint-config-prettier: ^8.5.0 - eslint-config-turbo: ^0.0.7 - eslint-plugin-react: ^7.32.2 - eslint-plugin-react-hooks: ^4.6.0 - eslint-plugin-tailwindcss: ^3.8.3 - devDependencies: - '@typescript-eslint/eslint-plugin': 5.51.0_yzj2n2b43wonjwaifya6xmk2zy - '@typescript-eslint/parser': 5.51.0_eslint@8.33.0 - eslint: 8.33.0 - eslint-config-prettier: 8.6.0_eslint@8.33.0 - eslint-config-turbo: 0.0.7_eslint@8.33.0 - eslint-plugin-react: 7.32.2_eslint@8.33.0 - eslint-plugin-react-hooks: 4.6.0_eslint@8.33.0 - eslint-plugin-tailwindcss: 3.8.3 - - packages/interface: + interface: specifiers: '@fontsource/inter': ^4.5.13 '@headlessui/react': ^1.7.3 @@ -450,7 +404,6 @@ importers: byte-size: ^8.1.0 class-variance-authority: ^0.4.0 clsx: ^1.2.1 - crypto-random-string: ^5.0.0 dayjs: ^1.11.5 phosphor-react: ^1.4.1 prettier: ^2.7.1 @@ -476,21 +429,21 @@ importers: dependencies: '@fontsource/inter': 4.5.15 '@headlessui/react': 1.7.7_biqbaboplfbrettd7655fr4n2y - '@hookform/resolvers': 2.9.10_react-hook-form@7.42.1 + '@hookform/resolvers': 2.9.10_react-hook-form@7.43.0 '@loadable/component': 5.15.2_react@18.2.0 '@radix-ui/react-progress': 1.0.1_biqbaboplfbrettd7655fr4n2y '@radix-ui/react-slider': 1.1.0_biqbaboplfbrettd7655fr4n2y '@radix-ui/react-toast': 1.1.2_biqbaboplfbrettd7655fr4n2y '@radix-ui/react-tooltip': 1.0.3_5ndqzdd6t4rivxsukjv3i3ak2q - '@sd/assets': link:../assets - '@sd/client': link:../client - '@sd/ui': link:../ui + '@sd/assets': link:../packages/assets + '@sd/client': link:../packages/client + '@sd/ui': link:../packages/ui '@sentry/browser': 7.31.1 '@splinetool/react-spline': 2.2.5_3ok4u2chf7465ntwlp4i32bwcu '@splinetool/runtime': 0.9.191 '@tailwindcss/forms': 0.5.3_tailwindcss@3.2.4 - '@tanstack/react-query': 4.22.0_biqbaboplfbrettd7655fr4n2y - '@tanstack/react-query-devtools': 4.22.0_gp275tdwez2a4eujt75dnnp754 + '@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y + '@tanstack/react-query-devtools': 4.22.0_pkeil6ml7pq7xvil3imldjs2sa '@tanstack/react-virtual': 3.0.0-beta.18_react@18.2.0 '@vitejs/plugin-react': 2.2.0_vite@4.0.4 '@zxcvbn-ts/core': 2.1.0 @@ -500,14 +453,13 @@ importers: byte-size: 8.1.0 class-variance-authority: 0.4.0_typescript@4.9.4 clsx: 1.2.1 - crypto-random-string: 5.0.0 dayjs: 1.11.7 phosphor-react: 1.4.1_react@18.2.0 react: 18.2.0 react-colorful: 5.6.1_biqbaboplfbrettd7655fr4n2y react-dom: 18.2.0_react@18.2.0 react-error-boundary: 3.1.4_react@18.2.0 - react-hook-form: 7.42.1_react@18.2.0 + react-hook-form: 7.43.0_react@18.2.0 react-json-view: 1.21.3_5ndqzdd6t4rivxsukjv3i3ak2q react-loading-skeleton: 3.1.0_react@18.2.0 react-qr-code: 2.0.11_react@18.2.0 @@ -520,7 +472,7 @@ importers: valtio: 1.9.0_react@18.2.0 zod: 3.20.2 devDependencies: - '@sd/config': link:../config + '@sd/config': link:../packages/config '@types/babel-core': 6.25.7 '@types/byte-size': 8.1.0 '@types/loadable__component': 5.13.4 @@ -533,6 +485,54 @@ importers: vite: 4.0.4_@types+node@18.11.18 vite-plugin-svgr: 2.4.0_vite@4.0.4 + packages/assets: + specifiers: {} + + packages/client: + specifiers: + '@rspc/client': ^0.0.0-main-7c0a67c1 + '@rspc/react': ^0.0.0-main-7c0a67c1 + '@sd/config': workspace:* + '@tanstack/react-query': ^4.12.0 + '@types/react': ^18.0.21 + crypto-random-string: ^5.0.0 + scripts: '*' + tsconfig: '*' + typescript: ^4.8.4 + valtio: ^1.7.4 + dependencies: + '@rspc/client': 0.0.0-main-7c0a67c1 + '@rspc/react': 0.0.0-main-7c0a67c1_tqsbl3x6ixew47ctvte2nz2yki + '@sd/config': link:../config + '@tanstack/react-query': 4.22.0 + crypto-random-string: 5.0.0 + valtio: 1.9.0 + devDependencies: + '@types/react': 18.0.27 + scripts: 0.1.0 + tsconfig: 7.0.0 + typescript: 4.9.4 + + packages/config: + specifiers: + '@typescript-eslint/eslint-plugin': ^5.51.0 + '@typescript-eslint/parser': ^5.51.0 + eslint: ^8.33.0 + eslint-config-prettier: ^8.5.0 + eslint-config-turbo: ^0.0.7 + eslint-plugin-react: ^7.32.2 + eslint-plugin-react-hooks: ^4.6.0 + eslint-plugin-tailwindcss: ^3.8.3 + devDependencies: + '@typescript-eslint/eslint-plugin': 5.51.0_yzj2n2b43wonjwaifya6xmk2zy + '@typescript-eslint/parser': 5.51.0_eslint@8.33.0 + eslint: 8.33.0 + eslint-config-prettier: 8.6.0_eslint@8.33.0 + eslint-config-turbo: 0.0.7_eslint@8.33.0 + eslint-plugin-react: 7.32.2_eslint@8.33.0 + eslint-plugin-react-hooks: 4.6.0_eslint@8.33.0 + eslint-plugin-tailwindcss: 3.8.3 + packages/ui: specifiers: '@babel/core': ^7.19.3 @@ -4113,12 +4113,12 @@ packages: tailwindcss: 3.2.4 dev: false - /@hookform/resolvers/2.9.10_react-hook-form@7.42.1: + /@hookform/resolvers/2.9.10_react-hook-form@7.43.0: resolution: {integrity: sha512-JIL1DgJIlH9yuxcNGtyhsWX/PgNltz+5Gr6+8SX9fhXc/hPbEIk6wPI82nhgvp3uUb6ZfAM5mqg/x7KR7NAb+A==} peerDependencies: react-hook-form: ^7.0.0 dependencies: - react-hook-form: 7.42.1_react@18.2.0 + react-hook-form: 7.43.0_react@18.2.0 dev: false /@humanwhocodes/config-array/0.11.8: @@ -7604,7 +7604,7 @@ packages: resolution: {integrity: sha512-9dqjv9eeB6VHN7lD3cLo16ZAjfjCsdXetSAD5+VyKqLUvcKTL0CklGQRJu+bWzdrS69R6Ea4UZo8obHYZnG6aA==} dev: false - /@tanstack/react-query-devtools/4.22.0_gp275tdwez2a4eujt75dnnp754: + /@tanstack/react-query-devtools/4.22.0_pkeil6ml7pq7xvil3imldjs2sa: resolution: {integrity: sha512-YeYFBnfqvb+ZlA0IiJqiHNNSzepNhI1p2o9i8NlhQli9+Zrn230M47OBaBUs8qr3DD1dC2zGB1Dis50Ktz8gAA==} peerDependencies: '@tanstack/react-query': 4.22.0 @@ -7612,7 +7612,7 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@tanstack/match-sorter-utils': 8.7.6 - '@tanstack/react-query': 4.22.0_biqbaboplfbrettd7655fr4n2y + '@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y react: 18.2.0 react-dom: 18.2.0_react@18.2.0 superjson: 1.12.2 @@ -7985,14 +7985,14 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 16.18.11 + '@types/node': 18.11.18 dev: true /@types/glob/8.0.0: resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 16.18.11 + '@types/node': 18.11.18 dev: true /@types/graceful-fs/4.1.6: @@ -8085,12 +8085,13 @@ packages: /@types/node-fetch/2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 16.18.11 + '@types/node': 18.11.18 form-data: 3.0.1 dev: true /@types/node/16.18.11: resolution: {integrity: sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==} + dev: true /@types/node/18.11.18: resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} @@ -8232,7 +8233,7 @@ packages: /@types/webpack-sources/3.2.0: resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==} dependencies: - '@types/node': 16.18.11 + '@types/node': 18.11.18 '@types/source-list-map': 0.1.2 source-map: 0.7.4 dev: true @@ -8240,7 +8241,7 @@ packages: /@types/webpack/4.41.33: resolution: {integrity: sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==} dependencies: - '@types/node': 16.18.11 + '@types/node': 18.11.18 '@types/tapable': 1.0.8 '@types/uglify-js': 3.17.1 '@types/webpack-sources': 3.2.0 @@ -14481,7 +14482,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 16.18.11 + '@types/node': 18.11.18 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -17531,6 +17532,15 @@ packages: react: 18.1.0 dev: false + /react-hook-form/7.43.0_react@18.2.0: + resolution: {integrity: sha512-/rVEz7T0gLdSFwPqutJ1kn2e0sQNyb9ci/hmwEYr2YG0KF/LSuRLvNrf9QWJM+gj88CjDpDW5Bh/1AD7B2+z9Q==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + react: 18.2.0 + dev: false + /react-inspector/5.1.1_react@18.2.0: resolution: {integrity: sha512-GURDaYzoLbW8pMGXwYPDBIv6nqei4kK7LPRZ9q9HCZF54wqXz/dnylBp/kfE9XmekBhHvLDdcYeyIwSrvtOiWg==} peerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 484f05707..0656b5b95 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,5 +2,6 @@ packages: - 'packages/*' - 'apps/*' - 'core' - - 'crates/sync/example/web' + - 'interface' - 'docs' + - 'crates/sync/example/web' diff --git a/tsconfig.json b/tsconfig.json index 56ce6a098..e38c963ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,10 +14,10 @@ "path": "apps/landing" }, { - "path": "packages/client" + "path": "interface" }, { - "path": "packages/interface" + "path": "packages/client" }, { "path": "packages/ui"