From 6199262886e21c954a60d6992b0307886059ecca Mon Sep 17 00:00:00 2001 From: Komediruzecki Date: Sat, 24 Apr 2021 15:56:52 +0200 Subject: [PATCH 01/23] Initial local space refactor and shared components update Implement topbar tree map Add local UI component Add breadcrumbs implementation Add shared modal implementation in local topbar Initial theme handling Add initial theme discard Add basic theme options to NavigatorItem Add initial theme setup for components Add navigator v2 themes Add folder list v2 themes Update some buttons via v2 button theme Add second iteration of theme updating Updated preferences tab components Updated topbar action items components Update bottom bar selection components Fix some preferences buttons and tabs Add shared styles for SearchModal component Add shared styles for preferences keymap tab Use shared global style Fix topbar breadcrumbs for specific storage cases Remove duplicate storage breadcrumbs where not needed Implement editing of storage name (localUI) Fix folder rename (localUI) Fix local UI for trashing and deleting note Fix no dialog on delete folder/note Update App.tsx with new dialog Fix local search colors Fix local search and local replace colors (v2 theme) Fix CodeEditor marks colors Discard local modal Discard local modal since cloud one is generic now Fix cloud intro modal Fix cloud intro modal colors Update Form to v2 style Update style functions for primary and secondary buttons Fix some size issues in folder view Fix folder navigation style (v2 update) Fix code fence styles Update code fence styles to v2 theme Fix shared context menu provider for local space Add shared context menu provider implementation in local space App Fix a typo in local space UI Implement proper topbar folder breadcrumbs Remove old topbar breadcrumbs on folder view Add folder page breadcrumbs when on folder view (no note view) Discard old topbar controls and use shared topbar controls (implement them based on previous) Comment out the toggle context view on topbar controls Add initial side bar mappers Add search API Update search structure for local space Implement sidebar mappers except for tree Half tree mapping done Add full tree implementation mock Todo: DND from local space Todo: Implement sidebar folding and other API Fix folder and note parent IDs Still need to fix some todos, styling and implement rest of items Implement Dnd Implement sidebar folding Add sidebar collapse store Fix recursion on folder Update v2 combined providers Reorganize storage sidebar items Fix storages, folders and note mappings Update sidebar to show workspace Update sidebar to show workspace and put notes and folders from storage to workspace root Rename tags to labels, note to doc and storage to workspace (v2 part of API, DB still not) Implement test DnD Refactor more v2 components to new naming Rename routes to doc and workspace, tags to lables and trashCan to archive Refactor sidebar Refactor sidebar application container Use shared app layout Implement rest of dnd Refactor more of application layout Implement proper sidebar toolbar instead of AppNavigator one Fix some search issues for matching note content Overall spaces mapping fix Fix context view component Fix topbar breadcrumb global style border issue Fix App navigator (revert) Add sidebar state in general status Implement timeline mapping for local Fix two bugs Fix topbar rendering fight on z-index with code mirror editor (set code mirror one to z-index: 0) Fix parent folder in breadcrumbs, should be set to breadcrumb folder so that new one goes inside Refactor code Refactor code into application structure Create Application main component Refactro WikiNotePage to use Application component Add separate ArchivePage Add separate AttachmentsPage Add separate LabelsPage Refactor some trash to archive namings Update to appropriate buttons on archive/label pages Update folder detail icon Use shared icon for folder detail view Set color of the icon and size for note/labels/folder/archive view Add timeline page Implement most of the components Fix timeline page Add archived at property Update events handling and show more button Set gray tags only Refactor code in timeline Fix DocLink (add style only) to not refresh the page as it is link with href and onClick prop Update preferences styling Update most of the styles for preferences Set all updated style to shared style functions Update some components Refactor AppNavigator Port features from AppNavigator Refactor CreateStoragePage Update Icon components in molecules and atoms in local space to use shared one Fix AttachmentsPage (no storage in Application component) Revert export buildSpacesBottomRows function Remove unused components from local Use Spinner from shared in cloud Removed bunch of now unused components (replaced by topbar and sidebar) Fix AppNavigator for cloud spaces For now using 2 navigators since its hard to refactor the code to use component in single place In local space there is need for sidebar and other items, in cloud space they are provided via webview In local space with toolbar off on sidebar it does not layout well with AppNavigator being hidden (even after z-index fix, it stays on top rather than move the sidebar) Refactor NoteStorageNavigator Remove unused code Rename component Update to shared toast Dialog/Modal/Toast from shared Remove few components Fix some bugs and error handling in localUI component Update topbar properties to have type for breadcrumbs (button and not separator) Update success messages across pushMessage new toast API Refactor FolderDetailListNoteItem Use WithTooltip and Button from shared components Add Search Highlight colors to shared common theme Update more components to shared Replace some buttons to use shared one Replace some links to use shared one (with inline encapsulation) Remove some unused components Refactor form buttons Refactor more components to shared Refactor AboutTab links Update storage create to shared form elements Rebase and update to latest master (new nav buttons) Update FormRowProps location in localUI Add sidebar hide option Update keymap component buttons Replace KeymapItemButton with shared one Fix global content search results and style Update to correct shared style colors for highlight Fix global search note and note content results (note content results not showing) Remove redundant console.logs from search results API Refactor more components to shared Update context view labels input and rendering (similar to cloud) Update ProgressBar to shared style theme Refactor form input to allow onKeyDown event as well Remove unused components after refactor --- .../organisms/Onboarding/UsagePage.tsx | 2 +- .../Subscription/SubscriptionManagement.tsx | 2 +- .../lib/hooks/useCloudResourceModals.tsx | 2 +- src/cloud/lib/i18n/enUS.ts | 2 +- src/cloud/lib/i18n/types.ts | 2 +- src/cloud/lib/mappers/topbarTree.ts | 2 +- src/cloud/lib/stores/pageStore/store.tsx | 1 - src/components/App.tsx | 136 +-- src/components/Application.tsx | 202 +++++ src/components/PreferencesModal/AboutTab.tsx | 31 +- src/components/PreferencesModal/KeymapTab.tsx | 34 +- .../PreferencesModal/MarkdownTab.tsx | 4 +- .../PreferencesModal/PreferencesModal.tsx | 32 +- .../PreferencesModal/StorageTab.tsx | 36 +- .../TabButton.tsx} | 0 src/components/PreferencesModal/styled.tsx | 36 +- src/components/Router.tsx | 45 +- src/components/Toast/ToastItem.tsx | 74 -- src/components/Toast/ToastList.tsx | 28 - src/components/Toast/index.ts | 1 - src/components/Toast/styled.tsx | 51 -- src/components/atoms/AppLink.tsx | 51 -- src/components/atoms/BoostHubWebview.tsx | 2 +- src/components/atoms/BottomBarButton.tsx | 13 +- src/components/atoms/ButtonIcon.tsx | 29 - src/components/atoms/CodeEditor.tsx | 18 +- .../FolderDetailListItemControlButton.tsx | 46 +- src/components/atoms/FolderTreeListItem.tsx | 79 -- src/components/atoms/FormFolderSelector.tsx | 73 +- src/components/atoms/HighlightText.tsx | 39 - src/components/atoms/InlineLink.tsx | 26 + src/components/atoms/InlineLinkButton.tsx | 15 - src/components/atoms/KeymapItemSection.tsx | 27 +- src/components/atoms/Link.tsx | 65 -- src/components/atoms/NavigatorButton.tsx | 59 -- src/components/atoms/NavigatorHeader.tsx | 108 --- src/components/atoms/NavigatorItem.tsx | 210 ----- src/components/atoms/NavigatorSeparator.tsx | 11 - .../atoms/NoteDetailNavigatorItem.tsx | 25 - .../atoms/PageScrollableContent.tsx | 9 - src/components/atoms/ProgressBar.tsx | 6 +- src/components/atoms/Spacer.tsx | 7 - src/components/atoms/Spinner.tsx | 25 - src/components/atoms/StorageLayout.tsx | 33 - src/components/atoms/TagNavigatorListItem.tsx | 10 +- .../atoms/TagNavigatorNewTagPopup.tsx | 169 ++-- src/components/atoms/ToolbarButton.tsx | 82 -- src/components/atoms/ToolbarButtonGroup.tsx | 21 - src/components/atoms/ToolbarIconButton.tsx | 64 -- src/components/atoms/ToolbarSeparator.tsx | 10 - .../atoms/ToolbarSlashSeparator.tsx | 24 - src/components/atoms/TopbarSwitchSelector.tsx | 68 -- src/components/atoms/TwoPaneLayout.tsx | 160 ---- src/components/atoms/dialog/DialogIcon.tsx | 33 - src/components/atoms/form.tsx | 64 +- src/components/atoms/markdown/CodeFence.tsx | 10 +- .../atoms/search/LocalSearchButton.tsx | 16 +- .../atoms/search/SearchResultItem.tsx | 2 +- .../AppNavigatorBoostHubTeamItem.tsx | 173 ---- .../molecules/AppNavigatorStorageItem.tsx | 204 ----- .../molecules/BookmarkNavigatorFragment.tsx | 90 -- .../molecules/EditorKeyMapSelect.tsx | 2 +- .../molecules/EditorSelectionStatus.tsx | 4 +- .../molecules/EditorThemeSelect.tsx | 2 +- .../molecules/FolderDetailListItem.tsx | 25 +- .../molecules/FolderDetailListNoteItem.tsx | 15 +- .../molecules/FolderNavigatorFragment.tsx | 134 --- .../molecules/FolderNoteNavigatorFragment.tsx | 269 ------ .../molecules/MessageBoxDialogBody.tsx | 72 -- src/components/molecules/ModalContainer.tsx | 4 +- .../molecules/NoteDetailTagNavigator.tsx | 92 +- src/components/molecules/NoteItem.tsx | 347 -------- .../molecules/NoteNavigatorItem.tsx | 97 -- .../molecules/NotePageToolbarFolderHeader.tsx | 70 -- .../molecules/NotePageToolbarNoteHeader.tsx | 352 -------- src/components/molecules/PromptDialogBody.tsx | 92 -- .../molecules/SearchModalNoteResultItem.tsx | 42 +- src/components/molecules/TagListFragment.tsx | 157 ---- .../molecules/Timeline/TimelineList.tsx | 148 +++ .../molecules/Timeline/TimelineListItem.tsx | 194 ++++ src/components/organisms/AppNavigator.tsx | 117 ++- .../{TrashDetail.tsx => ArchiveDetail.tsx} | 24 +- src/components/organisms/AttachmentList.tsx | 23 +- .../organisms/BoostHubIntroModal.tsx | 111 --- .../organisms/BoostHubSignInForm.tsx | 8 +- src/components/organisms/CloudIntroModal.tsx | 49 +- .../organisms/ConvertPouchStorageForm.tsx | 1 - .../organisms/CreateWorkspaceModal.tsx | 21 +- src/components/organisms/Dialog.tsx | 45 - .../organisms/FSStorageCreateForm.tsx | 77 +- src/components/organisms/FolderDetail.tsx | 13 +- src/components/organisms/IdleNoteDetail.tsx | 92 -- src/components/organisms/LocalReplace.tsx | 12 +- src/components/organisms/LocalSearch.tsx | 11 +- src/components/organisms/NoteContextView.tsx | 54 +- src/components/organisms/NoteDetail.tsx | 14 +- .../organisms/NoteListNavigator.tsx | 245 ----- src/components/organisms/NotePageToolbar.tsx | 406 --------- .../organisms/NoteStorageNavigator.tsx | 348 -------- src/components/organisms/SearchModal.tsx | 126 +-- src/components/organisms/SidebarContainer.tsx | 842 ++++++++++++++++++ src/components/organisms/TagDetail.tsx | 14 +- src/components/pages/ArchivePage.tsx | 41 + src/components/pages/AttachmentsPage.tsx | 31 +- src/components/pages/BoostHubLoginPage.tsx | 2 +- src/components/pages/LabelsPage.tsx | 42 + src/components/pages/StorageCreatePage.tsx | 39 +- src/components/pages/TimelinePage.tsx | 269 ++++++ src/components/pages/WikiNotePage.tsx | 527 +++++++++-- .../v2/organisms/BasicInputFormLocal.tsx | 62 ++ src/index.tsx | 22 +- src/lib/db/FSNoteDb.ts | 4 +- src/lib/db/createStore.ts | 30 +- src/lib/db/patterns.ts | 9 + src/lib/db/types.ts | 1 + src/lib/db/utils.ts | 66 ++ src/lib/dnd.ts | 8 +- src/lib/generalStatus.ts | 5 + src/lib/localStorageKeys.ts | 1 + src/lib/preferences.ts | 20 +- src/lib/routeParams.ts | 77 +- src/lib/search/search.ts | 35 +- src/lib/styled/BaseTheme.ts | 4 + src/lib/styled/styleFunctions.ts | 24 - src/lib/styled/styleFunctionsLocal.ts | 369 ++++++++ src/lib/v2/hooks/local/useLocalDB.ts | 267 ++++++ src/lib/v2/hooks/local/useLocalDnd.ts | 145 +++ src/lib/v2/hooks/local/useLocalUI.tsx | 369 ++++++++ src/lib/v2/interfaces/resources.ts | 22 + src/lib/v2/mappers/local/searchResults.ts | 140 +++ src/lib/v2/mappers/local/sidebarHistory.ts | 50 ++ src/lib/v2/mappers/local/sidebarRows.tsx | 75 ++ src/lib/v2/mappers/local/sidebarSpaces.ts | 22 + src/lib/v2/mappers/local/sidebarStorages.ts | 28 + src/lib/v2/mappers/local/sidebarTree.tsx | 583 ++++++++++++ src/lib/v2/mappers/local/timelineRows.ts | 38 + src/lib/v2/mappers/local/topbarBreadcrumbs.ts | 434 +++++++++ src/lib/v2/mappers/local/topbarTree.ts | 103 +++ src/lib/v2/stores/sidebarCollapse/index.tsx | 2 + src/lib/v2/stores/sidebarCollapse/store.tsx | 137 +++ src/lib/v2/stores/sidebarCollapse/types.ts | 17 + src/locales/csCZ.ts | 2 +- src/locales/de.ts | 2 +- src/locales/enUS.ts | 2 +- src/locales/esES.ts | 2 +- src/locales/fi.ts | 2 +- src/locales/frFR.ts | 2 +- src/locales/itIT.ts | 2 +- src/locales/ja.ts | 2 +- src/locales/ko.ts | 2 +- src/locales/ptBR.ts | 2 +- src/locales/ruRU.ts | 2 +- src/locales/ukUA.ts | 2 +- src/locales/zhCN.ts | 2 +- src/locales/zhHK.ts | 2 +- src/locales/zhTW.ts | 2 +- src/shared/components/atoms/Button.tsx | 2 +- src/shared/components/atoms/GlobalStyle.tsx | 22 +- src/shared/components/atoms/Link.tsx | 2 +- .../molecules/Form/atoms/FormInput.tsx | 4 + src/shared/lib/styled/common.ts | 4 + src/shared/lib/styled/styleFunctions.ts | 154 ++++ src/shared/lib/styled/types.ts | 3 + 163 files changed, 6361 insertions(+), 5650 deletions(-) create mode 100644 src/components/Application.tsx rename src/components/{pages/NotePage.tsx => PreferencesModal/TabButton.tsx} (100%) delete mode 100644 src/components/Toast/ToastItem.tsx delete mode 100644 src/components/Toast/ToastList.tsx delete mode 100644 src/components/Toast/index.ts delete mode 100644 src/components/Toast/styled.tsx delete mode 100644 src/components/atoms/AppLink.tsx delete mode 100644 src/components/atoms/ButtonIcon.tsx delete mode 100644 src/components/atoms/FolderTreeListItem.tsx delete mode 100644 src/components/atoms/HighlightText.tsx create mode 100644 src/components/atoms/InlineLink.tsx delete mode 100644 src/components/atoms/InlineLinkButton.tsx delete mode 100644 src/components/atoms/Link.tsx delete mode 100644 src/components/atoms/NavigatorButton.tsx delete mode 100644 src/components/atoms/NavigatorHeader.tsx delete mode 100644 src/components/atoms/NavigatorItem.tsx delete mode 100644 src/components/atoms/NavigatorSeparator.tsx delete mode 100644 src/components/atoms/NoteDetailNavigatorItem.tsx delete mode 100644 src/components/atoms/PageScrollableContent.tsx delete mode 100644 src/components/atoms/Spacer.tsx delete mode 100644 src/components/atoms/Spinner.tsx delete mode 100644 src/components/atoms/StorageLayout.tsx delete mode 100644 src/components/atoms/ToolbarButton.tsx delete mode 100644 src/components/atoms/ToolbarButtonGroup.tsx delete mode 100644 src/components/atoms/ToolbarIconButton.tsx delete mode 100644 src/components/atoms/ToolbarSeparator.tsx delete mode 100644 src/components/atoms/ToolbarSlashSeparator.tsx delete mode 100644 src/components/atoms/TopbarSwitchSelector.tsx delete mode 100644 src/components/atoms/TwoPaneLayout.tsx delete mode 100644 src/components/atoms/dialog/DialogIcon.tsx delete mode 100644 src/components/molecules/AppNavigatorBoostHubTeamItem.tsx delete mode 100644 src/components/molecules/AppNavigatorStorageItem.tsx delete mode 100644 src/components/molecules/BookmarkNavigatorFragment.tsx delete mode 100644 src/components/molecules/FolderNavigatorFragment.tsx delete mode 100644 src/components/molecules/FolderNoteNavigatorFragment.tsx delete mode 100644 src/components/molecules/MessageBoxDialogBody.tsx delete mode 100644 src/components/molecules/NoteItem.tsx delete mode 100644 src/components/molecules/NoteNavigatorItem.tsx delete mode 100644 src/components/molecules/NotePageToolbarFolderHeader.tsx delete mode 100644 src/components/molecules/NotePageToolbarNoteHeader.tsx delete mode 100644 src/components/molecules/PromptDialogBody.tsx delete mode 100644 src/components/molecules/TagListFragment.tsx create mode 100644 src/components/molecules/Timeline/TimelineList.tsx create mode 100644 src/components/molecules/Timeline/TimelineListItem.tsx rename src/components/organisms/{TrashDetail.tsx => ArchiveDetail.tsx} (88%) delete mode 100644 src/components/organisms/BoostHubIntroModal.tsx delete mode 100644 src/components/organisms/Dialog.tsx delete mode 100644 src/components/organisms/IdleNoteDetail.tsx delete mode 100644 src/components/organisms/NoteListNavigator.tsx delete mode 100644 src/components/organisms/NotePageToolbar.tsx delete mode 100644 src/components/organisms/NoteStorageNavigator.tsx create mode 100644 src/components/organisms/SidebarContainer.tsx create mode 100644 src/components/pages/ArchivePage.tsx create mode 100644 src/components/pages/LabelsPage.tsx create mode 100644 src/components/pages/TimelinePage.tsx create mode 100644 src/components/v2/organisms/BasicInputFormLocal.tsx create mode 100644 src/lib/db/patterns.ts create mode 100644 src/lib/styled/styleFunctionsLocal.ts create mode 100644 src/lib/v2/hooks/local/useLocalDB.ts create mode 100644 src/lib/v2/hooks/local/useLocalDnd.ts create mode 100644 src/lib/v2/hooks/local/useLocalUI.tsx create mode 100644 src/lib/v2/interfaces/resources.ts create mode 100644 src/lib/v2/mappers/local/searchResults.ts create mode 100644 src/lib/v2/mappers/local/sidebarHistory.ts create mode 100644 src/lib/v2/mappers/local/sidebarRows.tsx create mode 100644 src/lib/v2/mappers/local/sidebarSpaces.ts create mode 100644 src/lib/v2/mappers/local/sidebarStorages.ts create mode 100644 src/lib/v2/mappers/local/sidebarTree.tsx create mode 100644 src/lib/v2/mappers/local/timelineRows.ts create mode 100644 src/lib/v2/mappers/local/topbarBreadcrumbs.ts create mode 100644 src/lib/v2/mappers/local/topbarTree.ts create mode 100644 src/lib/v2/stores/sidebarCollapse/index.tsx create mode 100644 src/lib/v2/stores/sidebarCollapse/store.tsx create mode 100644 src/lib/v2/stores/sidebarCollapse/types.ts diff --git a/src/cloud/components/organisms/Onboarding/UsagePage.tsx b/src/cloud/components/organisms/Onboarding/UsagePage.tsx index 7240d90773..c3ba7c1d93 100644 --- a/src/cloud/components/organisms/Onboarding/UsagePage.tsx +++ b/src/cloud/components/organisms/Onboarding/UsagePage.tsx @@ -10,10 +10,10 @@ import { } from '@mdi/js' import Flexbox from '../../atoms/Flexbox' import Button from '../../atoms/Button' -import Spinner from '../../../../components/atoms/Spinner' import Icon from '../../../../components/atoms/Icon' import { useRouter } from '../../../lib/router' import { usingElectron, sendToHost } from '../../../lib/stores/electron' +import Spinner from '../../../../shared/components/atoms/Spinner' interface UsagePageProps { onUsage: (val: 'personal' | 'team') => void diff --git a/src/cloud/components/organisms/Subscription/SubscriptionManagement.tsx b/src/cloud/components/organisms/Subscription/SubscriptionManagement.tsx index 3ce515b54b..a38d49c2fd 100644 --- a/src/cloud/components/organisms/Subscription/SubscriptionManagement.tsx +++ b/src/cloud/components/organisms/Subscription/SubscriptionManagement.tsx @@ -1,7 +1,7 @@ import { mdiGiftOff, mdiOpenInNew } from '@mdi/js' import React, { useCallback, useMemo, useState } from 'react' import Icon from '../../../../components/atoms/Icon' -import Spinner from '../../../../components/atoms/Spinner' +import Spinner from '../../../../shared/components/atoms/Spinner' import { useToast } from '../../../../shared/lib/stores/toast' import { cancelSubscription } from '../../../api/teams/subscription' import { getTeamPortalUrl } from '../../../api/teams/subscription/invoices' diff --git a/src/cloud/lib/hooks/useCloudResourceModals.tsx b/src/cloud/lib/hooks/useCloudResourceModals.tsx index 0f29e71629..753ec99351 100644 --- a/src/cloud/lib/hooks/useCloudResourceModals.tsx +++ b/src/cloud/lib/hooks/useCloudResourceModals.tsx @@ -248,7 +248,7 @@ export function useCloudResourceModals() { ) => { messageBox({ title: `Delete ${title}`, - message: `Are you sure to remove for good this content?`, + message: `Are you sure you want to remove this permanently?`, iconType: DialogIconTypes.Warning, buttons: [ { diff --git a/src/cloud/lib/i18n/enUS.ts b/src/cloud/lib/i18n/enUS.ts index 42b9b33c41..9083879eb3 100644 --- a/src/cloud/lib/i18n/enUS.ts +++ b/src/cloud/lib/i18n/enUS.ts @@ -6,7 +6,7 @@ const enTranslation: TranslationSource = { 'general.cancel': 'Cancel', 'general.update': 'Update', 'general.attachments': 'Attachments', - 'general.trash': 'Trash', + 'general.archive': 'Archive', 'general.allnote': 'All Notes', 'general.signin': 'Sign In', 'general.signinCheck': 'Signing in...', diff --git a/src/cloud/lib/i18n/types.ts b/src/cloud/lib/i18n/types.ts index c107449824..018ed96786 100644 --- a/src/cloud/lib/i18n/types.ts +++ b/src/cloud/lib/i18n/types.ts @@ -3,7 +3,7 @@ export type TranslationSource = { 'general.cancel': string 'general.update': string 'general.attachments': string - 'general.trash': string + 'general.archive': string 'general.allnote': string 'general.signin': string 'general.signinCheck': string diff --git a/src/cloud/lib/mappers/topbarTree.ts b/src/cloud/lib/mappers/topbarTree.ts index 4d2289d18a..7913403edc 100644 --- a/src/cloud/lib/mappers/topbarTree.ts +++ b/src/cloud/lib/mappers/topbarTree.ts @@ -57,6 +57,7 @@ export function mapTopbarTree( ) folders.forEach((folder) => { + if (folder.pathname == '/') return const folderId = getFolderId(folder) const href = `${process.env.BOOST_HUB_BASE_URL}${getFolderHref( folder, @@ -112,6 +113,5 @@ export function mapTopbarTree( }) items.set(parentId, parentArray) }) - return items } diff --git a/src/cloud/lib/stores/pageStore/store.tsx b/src/cloud/lib/stores/pageStore/store.tsx index 760a1328d7..dccdb4a2c9 100644 --- a/src/cloud/lib/stores/pageStore/store.tsx +++ b/src/cloud/lib/stores/pageStore/store.tsx @@ -201,7 +201,6 @@ function usePageDataStore(pageProps: any) { }, } } - const docCount = team.creationsCounter const trialIsOver = !team.trial overLimit = docCount >= freePlanDocLimit diff --git a/src/components/App.tsx b/src/components/App.tsx index 34ded94406..1713d14318 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,19 +1,14 @@ -import React, { useState, useEffect, useCallback } from 'react' +import React, { useState, useEffect, useCallback, useMemo } from 'react' import Router from './Router' -import GlobalStyle from './GlobalStyle' import { ThemeProvider } from 'styled-components' -import { selectTheme } from '../themes' -import Dialog from './organisms/Dialog' import { useDb } from '../lib/db' import PreferencesModal from './PreferencesModal/PreferencesModal' import { usePreferences } from '../lib/preferences' import '../lib/i18n' import '../lib/analytics' import CodeMirrorStyle from './CodeMirrorStyle' -import ToastList from './Toast' import styled from '../lib/styled' import { useEffectOnce } from 'react-use' -import AppNavigator from './organisms/AppNavigator' import { useRouter } from '../lib/router' import { values, keys } from '../lib/db/utils' import { @@ -51,8 +46,16 @@ import { useCreateWorkspaceModal } from '../lib/createWorkspaceModal' import CreateWorkspaceModal from './organisms/CreateWorkspaceModal' import { useStorageRouter } from '../lib/storageRouter' import ExternalStyle from './ExternalStyle' -import { useDialog, DialogIconTypes } from '../lib/dialog' import { selectV2Theme } from '../shared/lib/styled/styleFunctions' +import Modal from '../shared/components/organisms/Modal' +import GlobalStyle from '../shared/components/atoms/GlobalStyle' +import { DialogIconTypes, useDialog } from '../shared/lib/stores/dialog' +import Dialog from '../shared/components/organisms/Dialog/Dialog' +import ContextMenu from '../shared/components/molecules/ContextMenu' +import { useCloudIntroModal } from '../lib/cloudIntroModal' +import CloudIntroModal from './organisms/CloudIntroModal' +import AppNavigator from './organisms/AppNavigator' +import Toast from '../shared/components/organisms/Toast' const LoadingText = styled.div` margin: 30px; @@ -119,25 +122,32 @@ const App = () => { messageBox({ title: 'Sign In', message: 'Your cloud access token has been expired.', - buttons: ['Sign In Again', 'Cancel'], - defaultButtonIndex: 0, + buttons: [ + { + label: 'Sign In Again', + defaultButton: true, + onClick: () => { + push(`/app/boosthub/login`) + setTimeout(() => { + boostHubLoginRequestEventEmitter.dispatch() + }, 1000) + }, + }, + { + label: 'Cancel', + cancelButton: true, + onClick: () => { + setPreferences({ + 'cloud.user': undefined, + }) + setGeneralStatus({ + boostHubTeams: [], + }) + }, + }, + ], + // defaultButtonIndex: 0, iconType: DialogIconTypes.Warning, - onClose: (value: number | null) => { - if (value === 0) { - push(`/app/boosthub/login`) - setTimeout(() => { - boostHubLoginRequestEventEmitter.dispatch() - }, 1000) - return - } - - setPreferences({ - 'cloud.user': undefined, - }) - setGeneralStatus({ - boostHubTeams: [], - }) - }, }) return } @@ -419,35 +429,57 @@ const App = () => { toggleShowCreateWorkspaceModal, } = useCreateWorkspaceModal() + const { showingCloudIntroModal } = useCloudIntroModal() + + const activeBoostHubTeamDomain = useMemo(() => { + if (routeParams.name !== 'boosthub.teams.show') { + return null + } + return routeParams.domain + }, [routeParams]) + + const boosthubTeam = + activeBoostHubTeamDomain != null + ? generalStatus.boostHubTeams.find( + (team) => team.domain === activeBoostHubTeamDomain + ) + : null + return ( - - - { - event.preventDefault() - }} - > - {initialized ? ( - <> + + { + event.preventDefault() + }} + > + {initialized ? ( + <> + {boosthubTeam != null ? ( - - {showCreateWorkspaceModal && ( - - )} - - ) : ( - Loading Data... - )} - - - - - - - - + ) : ( + showingCloudIntroModal && + )} + + {showCreateWorkspaceModal && ( + + )} + + ) : ( + Loading Data... + )} + + + + + + + + + + + ) } diff --git a/src/components/Application.tsx b/src/components/Application.tsx new file mode 100644 index 0000000000..b2e34d60ca --- /dev/null +++ b/src/components/Application.tsx @@ -0,0 +1,202 @@ +import React, { useCallback, useEffect, useMemo } from 'react' +import ContentLayout, { + ContentLayoutProps, +} from '../shared/components/templates/ContentLayout' +import { SidebarState } from '../shared/lib/sidebar' +import { useRouter } from '../lib/router' +import { StorageNotesRouteParams, useRouteParams } from '../lib/routeParams' +import { mapTopBarTree } from '../lib/v2/mappers/local/topbarTree' +import { useDb } from '../lib/db' +import { useGeneralStatus } from '../lib/generalStatus' +import { useSearchModal } from '../lib/searchModal' +import { addIpcListener, removeIpcListener } from '../lib/electronOnly' +import SearchModal from './organisms/SearchModal' +import SidebarContainer from './organisms/SidebarContainer' +import ApplicationLayout from '../shared/components/molecules/ApplicationLayout' + +interface ApplicationProps { + content: ContentLayoutProps + className?: string + initialSidebarState?: SidebarState + hideSidebar?: boolean +} + +const Application = ({ + content: { topbar, ...content }, + children, + initialSidebarState, + hideSidebar, +}: React.PropsWithChildren) => { + const { storageMap } = useDb() + const routeParams = useRouteParams() as StorageNotesRouteParams + const { workspaceId } = routeParams + const storage = storageMap[workspaceId] + + const { push, goBack, goForward } = useRouter() + const { generalStatus, setGeneralStatus } = useGeneralStatus() + const { noteViewMode, preferredEditingViewMode } = generalStatus + const { bookmarkNote, unbookmarkNote } = useDb() + const { showSearchModal } = useSearchModal() + + const note = useMemo(() => { + if (storage == null) { + return undefined + } + switch (routeParams.name) { + case 'workspaces.notes': { + if (routeParams.noteId == null) { + return undefined + } + const note = storage.noteMap[routeParams.noteId] + if (note == null) { + return undefined + } + if (!note.folderPathname.includes(routeParams.folderPathname)) { + return undefined + } + return note + } + } + return undefined + }, [ + routeParams.folderPathname, + routeParams.name, + routeParams.noteId, + storage, + ]) + + const noteId = note?._id + + const topbarTree = useMemo(() => { + if (storage == null) { + return undefined + } + return mapTopBarTree(storage.noteMap, storage.folderMap, storage, push) + }, [push, storage]) + + const bookmark = useCallback(async () => { + if (noteId == null || storage == null) { + return + } + await bookmarkNote(storage.id, noteId) + }, [noteId, storage, bookmarkNote]) + + const unbookmark = useCallback(async () => { + if (noteId == null || storage == null) { + return + } + await unbookmarkNote(storage.id, noteId) + }, [storage, noteId, unbookmarkNote]) + + const selectEditMode = useCallback(() => { + setGeneralStatus({ + noteViewMode: 'edit', + preferredEditingViewMode: 'edit', + }) + }, [setGeneralStatus]) + + const selectSplitMode = useCallback(() => { + setGeneralStatus({ + noteViewMode: 'split', + preferredEditingViewMode: 'split', + }) + }, [setGeneralStatus]) + + const selectPreviewMode = useCallback(() => { + setGeneralStatus({ + noteViewMode: 'preview', + }) + }, [setGeneralStatus]) + + const togglePreviewMode = useCallback(() => { + if (noteViewMode === 'preview') { + if (preferredEditingViewMode === 'edit') { + selectEditMode() + } else { + selectSplitMode() + } + } else { + selectPreviewMode() + } + }, [ + noteViewMode, + preferredEditingViewMode, + selectEditMode, + selectSplitMode, + selectPreviewMode, + ]) + + useEffect(() => { + addIpcListener('toggle-preview-mode', togglePreviewMode) + return () => { + removeIpcListener('toggle-preview-mode', togglePreviewMode) + } + }, [togglePreviewMode]) + + const toggleSplitEditMode = useCallback(() => { + if (noteViewMode === 'edit') { + selectSplitMode() + } else { + selectEditMode() + } + }, [noteViewMode, selectSplitMode, selectEditMode]) + + useEffect(() => { + addIpcListener('toggle-split-edit-mode', toggleSplitEditMode) + return () => { + removeIpcListener('toggle-split-edit-mode', toggleSplitEditMode) + } + }, [toggleSplitEditMode]) + + const toggleBookmark = useCallback(() => { + if (note == null) { + return + } + if (note.data.bookmarked) { + unbookmark() + } else { + bookmark() + } + }, [note, unbookmark, bookmark]) + + useEffect(() => { + addIpcListener('toggle-bookmark', toggleBookmark) + return () => { + removeIpcListener('toggle-bookmark', toggleBookmark) + } + }) + + return ( + <> + {storage != null && showSearchModal && } + + } + pageBody={ + <> + + {children} + + + } + /> + + ) +} + +export default Application diff --git a/src/components/PreferencesModal/AboutTab.tsx b/src/components/PreferencesModal/AboutTab.tsx index 0f4d8f85ee..6015af78a2 100644 --- a/src/components/PreferencesModal/AboutTab.tsx +++ b/src/components/PreferencesModal/AboutTab.tsx @@ -1,15 +1,12 @@ import React, { useCallback } from 'react' -import styled from '../../lib/styled' -import { - Section, - SectionHeader, - SectionSubtleText, - PrimaryAnchor, -} from './styled' +import { Section, SectionHeader, SectionSubtleText } from './styled' import { openNew } from '../../lib/platform' import Image from '../atoms/Image' import { useTranslation } from 'react-i18next' import SubscribeNewsLettersForm from '../organisms/SubscribeNewsLettersForm' +import styled from '../../shared/lib/styled' +import Link from '../../shared/components/atoms/Link' +import cc from 'classcat' const AboutContents = styled.div` max-width: 360px; @@ -82,6 +79,14 @@ interface PrimaryLinkProps { children: string } +const PrimaryLinkContainer = styled.div` + .about__tab__primary__link { + :hover { + text-decoration: underline; + } + } +` + const PrimaryLink = ({ href, children }: PrimaryLinkProps) => { const handleClick = useCallback( (event: React.MouseEvent) => { @@ -92,9 +97,15 @@ const PrimaryLink = ({ href, children }: PrimaryLinkProps) => { ) return ( - - {children} - + + + {children} + + ) } diff --git a/src/components/PreferencesModal/KeymapTab.tsx b/src/components/PreferencesModal/KeymapTab.tsx index 5708fd2393..dbd4d5884e 100644 --- a/src/components/PreferencesModal/KeymapTab.tsx +++ b/src/components/PreferencesModal/KeymapTab.tsx @@ -4,7 +4,8 @@ import { usePreferences } from '../../lib/preferences' import { getGenericShortcutString, KeymapItem } from '../../lib/keymap' import { useTranslation } from 'react-i18next' import KeymapItemSection from '../atoms/KeymapItemSection' -import styled from '../../lib/styled/styled' +import styled from '../../shared/lib/styled' +import Button from '../../shared/components/atoms/Button' const KeymapTab = () => { const { @@ -33,9 +34,9 @@ const KeymapTab = () => { {t('preferences.keymap')} - resetKeymap()}> + @@ -73,31 +74,4 @@ const SectionResetKeymap = styled.div` align-self: center; ` -export const KeymapItemButton = styled.button` - min-width: 88px; - max-width: 120px; - height: 32px; - font-size: 15px; - display: flex; - align-items: center; - justify-content: center; - - cursor: pointer; - - background-color: ${({ theme }) => theme.primaryButtonBackgroundColor}; - border: 1px solid ${({ theme }) => theme.borderColor}; - border-radius: 4px; - - transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.primaryButtonLabelColor}; - - text-align: center; - padding: 5px; - - &:hover { - border-color: ${({ theme }) => theme.borderColor}; - background: ${({ theme }) => theme.primaryButtonHoverBackgroundColor}; - } -` - export default KeymapTab diff --git a/src/components/PreferencesModal/MarkdownTab.tsx b/src/components/PreferencesModal/MarkdownTab.tsx index ea4ba8043a..3bfe6d8aa7 100644 --- a/src/components/PreferencesModal/MarkdownTab.tsx +++ b/src/components/PreferencesModal/MarkdownTab.tsx @@ -10,14 +10,14 @@ import { import CustomizedCodeEditor from '../atoms/CustomizedCodeEditor' import CustomizedMarkdownPreviewer from '../atoms/CustomizedMarkdownPreviewer' import { usePreferences } from '../../lib/preferences' -import styled from '../../lib/styled' import { SelectChangeEventHandler } from '../../lib/events' import { themes } from '../../lib/CodeMirror' import { capitalize } from '../../lib/string' import { useTranslation } from 'react-i18next' import { usePreviewStyle, defaultPreviewStyle } from '../../lib/preview' -import { borderRight, border } from '../../lib/styled/styleFunctions' import { FormCheckItem } from '../atoms/form' +import styled from '../../shared/lib/styled' +import { border, borderRight } from '../../shared/lib/styled/styleFunctions' const EditorContainer = styled.div` ${border} diff --git a/src/components/PreferencesModal/PreferencesModal.tsx b/src/components/PreferencesModal/PreferencesModal.tsx index 58deb1808f..4d076be9b4 100644 --- a/src/components/PreferencesModal/PreferencesModal.tsx +++ b/src/components/PreferencesModal/PreferencesModal.tsx @@ -1,21 +1,12 @@ import React, { useMemo, useCallback } from 'react' -import styled from '../../lib/styled' import { usePreferences } from '../../lib/preferences' import { useGlobalKeyDownHandler } from '../../lib/keyboard' import GeneralTab from './GeneralTab' import EditorTab from './EditorTab' import MarkdownTab from './MarkdownTab' import AboutTab from './AboutTab' -import { - backgroundColor, - closeIconColor, - border, - flexCenter, - borderBottom, - borderLeft, -} from '../../lib/styled/styleFunctions' +import { flexCenter } from '../../lib/styled/styleFunctions' import { useTranslation } from 'react-i18next' -import Icon from '../atoms/Icon' import { mdiClose, mdiHammerWrench } from '@mdi/js' import { useDb } from '../../lib/db' import { useRouteParams } from '../../lib/routeParams' @@ -23,7 +14,16 @@ import StorageTab from './StorageTab' import MigrationPage from './MigrationTab' import { useMigrations } from '../../lib/migrate/store' import KeymapTab from './KeymapTab' +import styled from '../../shared/lib/styled' +import { + border, + backgroundColor, + borderBottom, + borderLeft, + closeIconColor, +} from '../../shared/lib/styled/styleFunctions' import SettingNavButtonItem from '../../shared/components/organisms/Settings/atoms/SettingNavItem' +import Icon from '../../shared/components/atoms/Icon' const FullScreenContainer = styled.div` z-index: 7000; @@ -112,11 +112,11 @@ const PreferencesModal = () => { const currentStorage = useMemo(() => { let storageId: string switch (routeParams.name) { - case 'storages.notes': - case 'storages.tags.show': - case 'storages.attachments': - case 'storages.trashCan': - storageId = routeParams.storageId + case 'workspaces.notes': + case 'workspaces.labels.show': + case 'workspaces.attachments': + case 'workspaces.archive': + storageId = routeParams.workspaceId break default: return null @@ -168,7 +168,7 @@ const PreferencesModal = () => { - + {t('preferences.general')} diff --git a/src/components/PreferencesModal/StorageTab.tsx b/src/components/PreferencesModal/StorageTab.tsx index a57d3a0360..0500df39e7 100644 --- a/src/components/PreferencesModal/StorageTab.tsx +++ b/src/components/PreferencesModal/StorageTab.tsx @@ -3,7 +3,6 @@ import { useDb } from '../../lib/db' import { NoteStorage } from '../../lib/db/types' import { useRouter } from '../../lib/router' import { useDialog, DialogIconTypes } from '../../lib/dialog' -import { useToast } from '../../lib/toast' import { useTranslation } from 'react-i18next' import { FormHeading, @@ -24,7 +23,10 @@ import { boostHubPricingPageUrl, } from '../../lib/boosthub' import Alert from '../atoms/Alert' -import InlineLinkButton from '../atoms/InlineLinkButton' +import { useToast } from '../../shared/lib/stores/toast' +import Button from '../../shared/components/atoms/Button' +import styled from '../../shared/lib/styled' +import InlineLink from '../atoms/InlineLink' interface StorageEditPageProps { storage: NoteStorage @@ -101,7 +103,7 @@ const StorageEditPage = ({ storage }: StorageEditPageProps) => { so you can access useful features like document revision history, public APIs, document public sharing, 2000 tools integration and more. Please click{' '} - { event.preventDefault() openNew(boostHubLearnMorePageUrl) @@ -109,13 +111,13 @@ const StorageEditPage = ({ storage }: StorageEditPageProps) => { href={boostHubLearnMorePageUrl} > here - {' '} + {' '} to check it out.
  • Some features are limited based on your pricing plan. Please try Pro trial to access all of them for one week for free. Check our{' '} - { event.preventDefault() openNew(boostHubPricingPageUrl) @@ -123,7 +125,7 @@ const StorageEditPage = ({ storage }: StorageEditPageProps) => { href={boostHubPricingPageUrl} > pricing plan - {' '} + {' '} to know more.
  • @@ -164,9 +166,10 @@ const StorageEditPage = ({ storage }: StorageEditPageProps) => { Remove Space

    + {/*todo: Should be removed once pouch DB no longer active */} {storage.type !== 'fs' ? ( <> - This will permantly remove all notes locally stored in this space. + This will permanently remove all notes locally stored in this space. ) : ( <> @@ -175,10 +178,27 @@ const StorageEditPage = ({ storage }: StorageEditPageProps) => { )}   - Remove + + +

    ) } +const InlineLinkButton = styled.a` + .storage__tab__link { + cursor: pointer; + &:hover { + text-decoration: underline; + } + } +` + export default StorageEditPage diff --git a/src/components/pages/NotePage.tsx b/src/components/PreferencesModal/TabButton.tsx similarity index 100% rename from src/components/pages/NotePage.tsx rename to src/components/PreferencesModal/TabButton.tsx diff --git a/src/components/PreferencesModal/styled.tsx b/src/components/PreferencesModal/styled.tsx index aea7ca04da..1bee300984 100644 --- a/src/components/PreferencesModal/styled.tsx +++ b/src/components/PreferencesModal/styled.tsx @@ -1,13 +1,11 @@ -import styled from '../../lib/styled' +import styled from '../../shared/lib/styled' import { selectStyle, primaryButtonStyle, secondaryButtonStyle, inputStyle, tableStyle, - disabledUiTextColor, - PrimaryTextColor, -} from '../../lib/styled/styleFunctions' +} from '../../shared/lib/styled/styleFunctions' export const Section = styled.section` margin-bottom: 2em; @@ -19,11 +17,7 @@ export const SectionHeader = styled.h3` ` export const SectionSubtleText = styled.p` - ${disabledUiTextColor} -` - -export const PrimaryAnchor = styled.a` - ${PrimaryTextColor} + color: ${({ theme }) => theme.colors.text.disabled}; ` export const SectionMargin = styled.section` @@ -83,24 +77,6 @@ export const SectionTable = styled.table` ${tableStyle} ` -export const RightMargin = styled.span` - margin-right: 20px; -` - -export const TopMargin = styled.div` - margin-top: 40px; -` - -export const DeleteStorageButton = styled.button` - ${secondaryButtonStyle}; - padding: 0 16px; - height: 40px; - border-radius: 2px; - cursor: pointer; - vertical-align: middle; - align-items: center; -` - export const SectionListSelect = styled.div` ${selectStyle}; padding: 0 16px; @@ -111,8 +87,8 @@ export const SectionListSelect = styled.div` ` export const SearchMatchHighlight = styled.span` - background-color: ${({ theme }) => theme.searchHighlightBackgroundColor}; - color: ${({ theme }) => theme.searchHighlightTextColor}; - + background-color: ${({ theme }) => + theme.codeEditorSelectedTextBackgroundColor}; + color: #212121 !important; padding: 2px; ` diff --git a/src/components/Router.tsx b/src/components/Router.tsx index 751e2c74cd..628d4c3ecf 100644 --- a/src/components/Router.tsx +++ b/src/components/Router.tsx @@ -19,6 +19,9 @@ import { openNew } from '../lib/platform' import BoostHubLoginPage from './pages/BoostHubLoginPage' import { ObjectMap, NoteStorage } from '../lib/db/types' import { useGeneralStatus } from '../lib/generalStatus' +import ArchivePage from './pages/ArchivePage' +import LabelsPage from './pages/LabelsPage' +import TimelinePage from './pages/TimelinePage' const NotFoundPageContainer = styled.div` padding: 15px 25px; @@ -119,26 +122,50 @@ function useContent( return case 'boosthub.teams.show': return null - case 'storages.notes': - case 'storages.trashCan': - case 'storages.tags.show': { - const { storageId } = routeParams - const storage = storageMap[storageId] + case 'workspaces.notes': { + const { workspaceId } = routeParams + const storage = storageMap[workspaceId] if (storage == null) { break } return } - case 'storages.attachments': { - const { storageId } = routeParams - const storage = storageMap[storageId] + + case 'workspaces.labels.show': { + const { workspaceId, tagName } = routeParams + const storage = storageMap[workspaceId] + if (storage == null) { + break + } + return + } + + case 'workspaces.archive': { + const { workspaceId } = routeParams + const storage = storageMap[workspaceId] + if (storage == null) { + break + } + return + } + case 'workspaces.attachments': { + const { workspaceId } = routeParams + const storage = storageMap[workspaceId] if (storage == null) { break } return } - case 'storages.create': + case 'workspaces.timeline': { + const { workspaceId } = routeParams + const storage = storageMap[workspaceId] + if (storage == null) { + break + } + return + } + case 'workspaces.create': return } return ( diff --git a/src/components/Toast/ToastItem.tsx b/src/components/Toast/ToastItem.tsx deleted file mode 100644 index f517168148..0000000000 --- a/src/components/Toast/ToastItem.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react' -import { ToastMessage } from '../../lib/toast' -import { - StyledToastContainer, - StyledToastTop, - StyledToastRight, - StyledToastTitle, - StyledToastCloseButton, - StyledToastDescription, -} from './styled' - -interface ToastItemProps { - item: ToastMessage - onClose: (item: ToastMessage) => void -} - -interface ToastItemState { - remaining: number - timer: any -} - -class ToastItem extends React.Component { - state = { - remaining: 3000, - timer: 0, - } - - componentDidMount() { - this.resumeTimer() - } - - componentWillUnmount() { - window.clearTimeout(this.state.timer) - } - - resumeTimer = () => { - window.clearTimeout(this.state.timer) - this.setState({ - timer: setTimeout(this.dismissMessage, this.state.remaining), - }) - } - - pauseTimer = () => { - clearTimeout(this.state.timer) - } - - dismissMessage = () => { - this.props.onClose(this.props.item) - } - - render() { - return ( - - - {this.props.item.title} - - - this.props.onClose(this.props.item)}> - × - - - - - - {this.props.item.description} - - - ) - } -} -export default ToastItem diff --git a/src/components/Toast/ToastList.tsx b/src/components/Toast/ToastList.tsx deleted file mode 100644 index cb192087a8..0000000000 --- a/src/components/Toast/ToastList.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react' -import { useToast } from '../../lib/toast' -import ToastItem from './ToastItem' -import styled from '../../lib/styled' - -const StyledToastList = styled.ul` - position: fixed; - z-index: 10000; - display: flex; - flex-direction: column-reverse; - right: 0; - bottom: 0; - list-style: none; -` - -export default () => { - const { messages, removeMessage } = useToast() - - return ( - - {messages.map((message) => ( -
  • - -
  • - ))} - - ) -} diff --git a/src/components/Toast/index.ts b/src/components/Toast/index.ts deleted file mode 100644 index 80b7990dd7..0000000000 --- a/src/components/Toast/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ToastList' diff --git a/src/components/Toast/styled.tsx b/src/components/Toast/styled.tsx deleted file mode 100644 index 913e7bceda..0000000000 --- a/src/components/Toast/styled.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import styled from '../../lib/styled' -import { secondaryBackgroundColor } from '../../lib/styled/styleFunctions' - -export const StyledToastContainer = styled.div` - width: 350px; - margin: 40px; - padding: 10px 30px; - box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); - border-radius: 5px; - ${secondaryBackgroundColor} -` -export const StyledToastTop = styled.div` - display: inline-flex; - border-bottom: 1px solid; - width: 100%; -` -export const StyledToastRight = styled.div` - position: absolute; - right: 60px; - display: inline-flex; -` -export const StyledToastTitle = styled.p` - font-size: 16px; - font-weight: 600; -` - -export const StyledToastTime = styled.p` - font-size: 12px; - margin-right: 10px; - line-height: 25px; -` - -export const StyledToastCloseButton = styled.button` - background-color: transparent; - font-size: 24px; - order: none; - cursor: pointer; - border: none; - &:hover { - color: ${({ theme }) => theme.navButtonHoverColor}; - } - - &:active, - .active { - color: ${({ theme }) => theme.navButtonActiveColor}; - } -` - -export const StyledToastDescription = styled.p` - font-size: 14px; -` diff --git a/src/components/atoms/AppLink.tsx b/src/components/atoms/AppLink.tsx deleted file mode 100644 index 0513bd20c2..0000000000 --- a/src/components/atoms/AppLink.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react' -import isElectron from 'is-electron' -import { getAppLinkFromUserAgent } from '../../lib/download' -import { openNew } from '../../lib/platform' -import styled from '../../lib/styled' -import cc from 'classcat' -import { primaryButtonStyle } from '../../lib/styled/styleFunctions' - -const AppLinkContainer = styled.button` - ${primaryButtonStyle} - padding: 0 16px; - height: 40px; - line-height: 1; - border-width: initial; - border-style: none; - border-color: initial; - border-image: initial; - border-radius: 2px; - margin-bottom: 10px; - - .subtext { - font-size: 12px; - } -` - -interface AppLinkProps { - className?: string -} -const AppLink = ({ className }: AppLinkProps) => { - const runningOnElectron = isElectron() - const userAgent = getAppLinkFromUserAgent() - - const handleClick = (event: React.MouseEvent) => { - event.preventDefault() - openNew(runningOnElectron ? 'https://note.boostio.co/app' : userAgent.link) - } - - return ( - - {runningOnElectron - ? 'Open in browser' - : `Download ${userAgent.os !== '' ? `for ${userAgent.os}` : 'our app'}`} - - ) -} - -export default AppLink diff --git a/src/components/atoms/BoostHubWebview.tsx b/src/components/atoms/BoostHubWebview.tsx index 024ad49e17..250a31f7de 100644 --- a/src/components/atoms/BoostHubWebview.tsx +++ b/src/components/atoms/BoostHubWebview.tsx @@ -18,7 +18,6 @@ import { NewWindowEvent, } from 'electron' import { useEffectOnce } from 'react-use' -import styled from '../../lib/styled' import { openNew } from '../../lib/platform' import { boostHubNavigateRequestEventEmitter, @@ -35,6 +34,7 @@ import { import { usePreferences } from '../../lib/preferences' import { openContextMenu, openExternal } from '../../lib/electronOnly' import { DidFailLoadEvent } from 'electron/main' +import styled from '../../shared/lib/styled' export interface WebviewControl { focus(): void diff --git a/src/components/atoms/BottomBarButton.tsx b/src/components/atoms/BottomBarButton.tsx index f2a05a565f..68e707dc0f 100644 --- a/src/components/atoms/BottomBarButton.tsx +++ b/src/components/atoms/BottomBarButton.tsx @@ -1,6 +1,7 @@ import React, { MouseEventHandler, FC } from 'react' -import styled from '../../lib/styled' -import { flexCenter, borderLeft } from '../../lib/styled/styleFunctions' +import { flexCenter } from '../../lib/styled/styleFunctions' +import styled from '../../shared/lib/styled' +import { borderLeft } from '../../shared/lib/styled/styleFunctions' interface BottomBarButtonProps { className?: string @@ -24,13 +25,13 @@ export default BottomBarButton const Container = styled.button` background-color: transparent; border: none; - color: ${({ theme }) => theme.uiTextColor}; + color: ${({ theme }) => theme.colors.text.primary}; font-size: 14px; - ${flexCenter} + ${flexCenter}; padding: 0 5px; - ${borderLeft} + ${borderLeft}; cursor: pointer; &:hover { - background-color: ${({ theme }) => theme.navItemHoverBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.secondary}; } ` diff --git a/src/components/atoms/ButtonIcon.tsx b/src/components/atoms/ButtonIcon.tsx deleted file mode 100644 index d0c5334e5d..0000000000 --- a/src/components/atoms/ButtonIcon.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react' -import styled from '../../lib/styled' -import Icon from './Icon' - -const StyledButtonIcon = styled.button` - color: currentColor; - background-color: transparent; - border: none; - cursor: pointer; - - svg { - margin-top: 2px; - vertical-align: top; - } -` - -interface ButtonIconProps { - iconPath: string - className?: string - onClick?: () => void -} - -const ButtonIcon = ({ iconPath, className, onClick }: ButtonIconProps) => ( - - - -) - -export default ButtonIcon diff --git a/src/components/atoms/CodeEditor.tsx b/src/components/atoms/CodeEditor.tsx index e48243dcde..9c463360cd 100644 --- a/src/components/atoms/CodeEditor.tsx +++ b/src/components/atoms/CodeEditor.tsx @@ -1,17 +1,18 @@ import React from 'react' import CodeMirror, { getCodeMirrorTheme } from '../../lib/CodeMirror' -import styled from '../../lib/styled' -import { borderRight } from '../../lib/styled/styleFunctions' import { EditorIndentTypeOptions, EditorIndentSizeOptions, EditorKeyMapOptions, } from '../../lib/preferences' import { osName } from '../../lib/platform' +import styled from '../../shared/lib/styled' +import { borderRight } from '../../shared/lib/styled/styleFunctions' const StyledContainer = styled.div` .CodeMirror { font-family: inherit; + z-index: 0 !important; } .CodeMirror-dialog button { @@ -21,7 +22,7 @@ const StyledContainer = styled.div` line-height: 26px; padding: 0 15px; transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.primaryDarkerColor}; + color: ${({ theme }) => theme.colors.text.primary}; border: none; ${borderRight} &:last-child { @@ -30,14 +31,14 @@ const StyledContainer = styled.div` } .CodeMirror-dialog button:hover { - color: ${({ theme }) => theme.primaryButtonLabelColor}; - background-color: ${({ theme }) => theme.primaryColor}; + color: ${({ theme }) => theme.colors.text.secondary}; + background-color: ${({ theme }) => theme.colors.background.primary}; } .marked { background-color: ${({ theme }) => - theme.searchHighlightSubtleBackgroundColor}; - color: ${({ theme }) => theme.searchHighlightTextColor} !important; + theme.codeEditorMarkedTextBackgroundColor}; + color: #212121 !important; padding: 3px; } @@ -47,7 +48,8 @@ const StyledContainer = styled.div` } .selected { - background-color: ${({ theme }) => theme.searchHighlightBackgroundColor}; + background-color: ${({ theme }) => + theme.codeEditorSelectedTextBackgroundColor}; border: 1px solid #fffae3; } ` diff --git a/src/components/atoms/FolderDetailListItemControlButton.tsx b/src/components/atoms/FolderDetailListItemControlButton.tsx index b6c94d40c1..301ba7c0ee 100644 --- a/src/components/atoms/FolderDetailListItemControlButton.tsx +++ b/src/components/atoms/FolderDetailListItemControlButton.tsx @@ -1,6 +1,6 @@ import React, { MouseEventHandler } from 'react' -import styled from '../../lib/styled' -import Icon from './Icon' +import Button from '../../shared/components/atoms/Button' +import WithTooltip from '../../shared/components/atoms/WithTooltip' interface FolderDetailListItemControlButtonProps { onClick?: MouseEventHandler @@ -16,39 +16,17 @@ const FolderDetailListItemControlButton = ({ active = false, }: FolderDetailListItemControlButtonProps) => { return ( - - - + + - - ) -} - -export default FolderTreeListItem - -const Container = styled.li` - & > button { - width: 100%; - height: 24px; - text-align: left; - background-color: ${({ theme }) => theme.navItemBackgroundColor}; - color: ${({ theme }) => theme.navButtonColor}; - border: none; - padding-right: 4px; - display: flex; - align-items: center; - & > .icon { - font-size: 18px; - flex-shrink: 0; - } - & > .folderIcon { - margin-right: 4px; - } - & > .label { - max-width: 100px; - ${textOverflow} - } - cursor: pointer; - &:hover { - background-color: ${({ theme }) => theme.navItemHoverBackgroundColor}; - } - &:active, - &.active { - background-color: ${({ theme }) => theme.navItemActiveBackgroundColor}; - } - } -` diff --git a/src/components/atoms/FormFolderSelector.tsx b/src/components/atoms/FormFolderSelector.tsx index 8b977ed127..516f89cc2c 100644 --- a/src/components/atoms/FormFolderSelector.tsx +++ b/src/components/atoms/FormFolderSelector.tsx @@ -1,17 +1,17 @@ -import React, { useCallback, useState, CSSProperties } from 'react' +import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import styled from '../../lib/styled' -import { border, secondaryButtonStyle } from '../../lib/styled/styleFunctions' import { getPathByName, showOpenDialog } from '../../lib/electronOnly' +import styled from '../../shared/lib/styled' +import { border } from '../../shared/lib/styled/styleFunctions' +import Form from '../../shared/components/molecules/Form' const FormFolderSelectorInput = styled.input` display: block; flex: 1; padding: 0.375rem 0.75rem; - line-height: 1.5; border-top-left-radius: 0.25rem; border-bottom-left-radius: 0.25rem; - ${border} + ${border}; background-color: white; cursor: pointer; &:disabled { @@ -20,29 +20,12 @@ const FormFolderSelectorInput = styled.input` } ` -const FormFolderSelectorContainer = styled.div` - display: flex; -` - -const FormFolderSelectorButton = styled.button` - ${secondaryButtonStyle} - padding: .375rem .75rem; - font-size: 1rem; - line-height: 1.5; - border-top-right-radius: 0.25rem; - border-bottom-right-radius: 0.25rem; - &:first-child { - margin-left: 0; - } -` - interface FormFolderSelector { value: string - style?: CSSProperties setValue: (value: string) => void } -const FormFolderSelector = ({ value, style, setValue }: FormFolderSelector) => { +const FormFolderSelector = ({ value, setValue }: FormFolderSelector) => { const [dialogIsOpen, setDialogIsOpen] = useState(false) const { t } = useTranslation() const openDialog = useCallback(async () => { @@ -72,19 +55,37 @@ const FormFolderSelector = ({ value, style, setValue }: FormFolderSelector) => { }, [dialogIsOpen, setValue, t]) return ( - - - - Select Folder - - +
    + ), + }, + { + type: 'button', + props: { + label: 'Select Folder', + variant: 'primary', + onClick: openDialog, + }, + }, + ], + }, + ]} + /> ) } diff --git a/src/components/atoms/HighlightText.tsx b/src/components/atoms/HighlightText.tsx deleted file mode 100644 index 16800f658f..0000000000 --- a/src/components/atoms/HighlightText.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useMemo } from 'react' -import styled from '../../lib/styled' -import { escapeRegExp } from '../../lib/string' - -const HighlightContainer = styled.span` - .highlighted { - background: rgba(3, 197, 136, 0.6); - } -` - -interface HighlightTextProps { - text: string - search: string -} - -function HighlightText(props: HighlightTextProps) { - const { text, search } = props - return useMemo(() => { - if (search.trim() === '') return <>{text} - const searchRegex = new RegExp(`(${escapeRegExp(search)})`, 'gi') - const parts = text.split(searchRegex) - - return ( - - {parts.map((part, i) => - part.toLowerCase() === search.toLowerCase() ? ( - - {part} - - ) : ( - {part} - ) - )} - - ) - }, [text, search]) -} - -export default HighlightText diff --git a/src/components/atoms/InlineLink.tsx b/src/components/atoms/InlineLink.tsx new file mode 100644 index 0000000000..6c8c54c971 --- /dev/null +++ b/src/components/atoms/InlineLink.tsx @@ -0,0 +1,26 @@ +import React, { MouseEventHandler } from 'react' +import cc from 'classcat' +import Link, { HyperLinkProps } from '../../shared/components/atoms/Link' +import styled from '../../shared/lib/styled' + +const InlineLink: React.FC = ({ + className, + children, + ...props +}) => { + return ( + + + {children} + + + ) +} + +export default InlineLink + +const InlineLinkContainer = styled.span` + .storage__link_style { + padding: 0 !important; + } +` diff --git a/src/components/atoms/InlineLinkButton.tsx b/src/components/atoms/InlineLinkButton.tsx deleted file mode 100644 index 84b01c52b1..0000000000 --- a/src/components/atoms/InlineLinkButton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import styled from '../../lib/styled' - -const InlineLinkButton = styled.a` - color: ${({ theme }) => theme.primaryColor}; - cursor: pointer; - &:hover { - text-decoration: underline; - } - - &.inline-link--variant-danger { - color: #ef5b5b; - } -` - -export default InlineLinkButton diff --git a/src/components/atoms/KeymapItemSection.tsx b/src/components/atoms/KeymapItemSection.tsx index 1a89786816..bc34456bb4 100644 --- a/src/components/atoms/KeymapItemSection.tsx +++ b/src/components/atoms/KeymapItemSection.tsx @@ -5,15 +5,15 @@ import React, { useRef, useState, } from 'react' -import styled from '../../lib/styled' import { getGenericShortcutString, KeymapItemEditableProps, } from '../../lib/keymap' -import { inputStyle } from '../../lib/styled/styleFunctions' import cc from 'classcat' -import { useToast } from '../../lib/toast' -import { KeymapItemButton } from '../PreferencesModal/KeymapTab' +import styled from '../../shared/lib/styled' +import { inputStyle } from '../../shared/lib/styled/styleFunctions' +import { useToast } from '../../shared/lib/stores/toast' +import Button from '../../shared/components/atoms/Button' const invalidShortcutInputs = [' '] const rejectedShortcutInputs = [' ', 'control', 'alt', 'shift'] @@ -152,23 +152,19 @@ const KeymapItemSection = ({ onKeyDown={fetchInputShortcuts} /> )} - + {changingShortcut && ( - - Cancel - + )} {currentShortcut != null && !changingShortcut && ( - - Un-assign - + )} @@ -184,9 +180,10 @@ const ShortcutItemStyle = styled.div` align-items: center; justify-content: center; - background-color: ${({ theme }) => theme.primaryButtonBackgroundColor}; - color: ${({ theme }) => theme.primaryButtonLabelColor}; - border: 1px solid ${({ theme }) => theme.borderColor}; + background-color: ${({ theme }) => theme.colors.background.tertiary}; + color: ${({ theme }) => theme.colors.text.primary}; + + border: 1px solid ${({ theme }) => theme.colors.border.main}; border-radius: 4px; ` diff --git a/src/components/atoms/Link.tsx b/src/components/atoms/Link.tsx deleted file mode 100644 index 4f717a1517..0000000000 --- a/src/components/atoms/Link.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { - useCallback, - FC, - CSSProperties, - MouseEventHandler, - FocusEventHandler, - DragEventHandler, -} from 'react' -import { useRouter } from '../../lib/router' - -export interface LinkProps { - href?: string - children: React.ReactNode - className?: string - style?: CSSProperties - onContextMenu?: MouseEventHandler - onFocus?: FocusEventHandler - draggable?: boolean - onDragStart?: DragEventHandler - onDrop?: DragEventHandler - onDragOver?: DragEventHandler -} - -const Link: FC = ({ - children, - href, - className, - style, - onContextMenu, - onFocus, - onDragStart, - onDragOver, - onDrop, -}) => { - const router = useRouter() - - const push = useCallback( - (e: React.MouseEvent) => { - e.preventDefault() - if (href != null) { - router.push(href) - } - }, - [href, router] - ) - - return ( - - {children} - - ) -} - -export default Link diff --git a/src/components/atoms/NavigatorButton.tsx b/src/components/atoms/NavigatorButton.tsx deleted file mode 100644 index 03a9d6b862..0000000000 --- a/src/components/atoms/NavigatorButton.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react' -import styled from '../../lib/styled' -import Icon from './Icon' - -const ButtonContainer = styled.button` - width: 24px; - height: 24px; - font-size: 18px; - display: flex; - align-items: center; - justify-content: center; - - background-color: transparent; - border-radius: 50%; - border: none; - cursor: pointer; - - transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.navButtonColor}; - &:hover { - color: ${({ theme }) => theme.navButtonHoverColor}; - } - - &:active, - &.active { - color: ${({ theme }) => theme.navButtonActiveColor}; - } -` - -interface NavigatorButtonProps { - active?: boolean - onClick?: React.MouseEventHandler - onContextMenu?: React.MouseEventHandler - iconPath: string - spin?: boolean - title?: string -} - -const NavigatorButton = ({ - active, - onClick, - onContextMenu, - iconPath, - title, - spin, -}: NavigatorButtonProps) => { - return ( - - - - ) -} - -export default NavigatorButton diff --git a/src/components/atoms/NavigatorHeader.tsx b/src/components/atoms/NavigatorHeader.tsx deleted file mode 100644 index b8eac8d1fb..0000000000 --- a/src/components/atoms/NavigatorHeader.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react' -import styled from '../../lib/styled' -import { textOverflow } from '../../lib/styled/styleFunctions' -import Icon from './Icon' -import { mdiChevronRight, mdiChevronDown } from '@mdi/js' - -const HeaderContainer = styled.header` - position: relative; - user-select: none; - height: 28px; - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - - font-size: 1em; - transition: 200ms background-color; - &:hover { - .control { - opacity: 1; - } - } - - &.visibleControl { - .control { - opacity: 1; - } - } -` - -const Label = styled.div` - flex: 1; - ${textOverflow} - &:first-child { - padding-left: 5px; - } -` - -const Control = styled.div` - position: absolute; - right: 0; - top: 2px; - display: flex; -` - -const ClickableContainer = styled.div` - background-color: transparent; - height: 28px; - border: none; - border-radius: 3px; - display: flex; - align-items: center; - text-align: left; - flex: 1; - overflow: hidden; - cursor: pointer; - color: ${({ theme }) => theme.disabledUiTextColor}; - background-color: ${({ theme }) => theme.navItemBackgroundColor}; - &:hover { - background-color: ${({ theme }) => theme.navItemHoverBackgroundColor}; - color: ${({ theme }) => theme.navItemColor}; - } - &:active, - &.active { - background-color: ${({ theme }) => theme.navItemActiveBackgroundColor}; - color: ${({ theme }) => theme.navItemColor}; - } - &:hover:active, - &:hover.active { - background-color: ${({ theme }) => theme.navItemHoverActiveBackgroundColor}; - } - - &.subtle { - color: ${({ theme }) => theme.disabledUiTextColor}; - } -` - -interface NavigatorHeaderProps { - label: string - active?: boolean - control?: React.ReactNode - onContextMenu?: React.MouseEventHandler - folded?: boolean - onClick?: React.MouseEventHandler -} - -const NavigatorHeader = ({ - folded, - label, - active = false, - onContextMenu, - onClick, - control, -}: NavigatorHeaderProps) => { - return ( - - - {folded != null && ( - - )} - - - {control && {control}} - - ) -} - -export default NavigatorHeader diff --git a/src/components/atoms/NavigatorItem.tsx b/src/components/atoms/NavigatorItem.tsx deleted file mode 100644 index ffde2fe51e..0000000000 --- a/src/components/atoms/NavigatorItem.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import React from 'react' -import cc from 'classcat' -import styled from '../../lib/styled' -import Icon from './Icon' -import { mdiChevronDown, mdiChevronRight, mdiCircleSmall } from '@mdi/js' -import { textOverflow } from '../../lib/styled/styleFunctions' - -const Container = styled.div` - position: relative; - user-select: none; - height: 28px; - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - - font-size: 1em; - transition: 200ms background-color; - &:hover { - .control { - opacity: 1; - } - } - - &.visibleControl { - .control { - opacity: 1; - } - } -` - -const FoldButton = styled.button` - position: absolute; - width: 24px; - height: 24px; - border: none; - background-color: transparent; - border-radius: 50%; - top: 2px; - display: flex; - align-items: center; - justify-content: center; - - transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.navButtonColor}; - &:hover { - color: ${({ theme }) => theme.navButtonHoverColor}; - } - - &:active, - .active { - color: ${({ theme }) => theme.navButtonActiveColor}; - } -` - -const ClickableContainer = styled.button` - background-color: transparent; - height: 28px; - border: none; - border-radius: 3px; - display: flex; - align-items: center; - text-align: left; - flex: 1; - overflow: hidden; - cursor: pointer; - - color: ${({ theme }) => theme.navItemColor}; - background-color: ${({ theme }) => theme.navItemBackgroundColor}; - &:hover { - background-color: ${({ theme }) => theme.navItemHoverBackgroundColor}; - } - &:active, - &.active { - background-color: ${({ theme }) => theme.navItemActiveBackgroundColor}; - } - &:hover:active, - &:hover.active { - background-color: ${({ theme }) => theme.navItemHoverActiveBackgroundColor}; - } - - &.subtle { - color: ${({ theme }) => theme.disabledUiTextColor}; - } -` - -const Label = styled.div` - ${textOverflow} - flex: 1; - font-size: 14px; - - &.subtle { - color: ${({ theme }) => theme.disabledUiTextColor}; - } -` - -const Control = styled.div` - position: absolute; - right: 0; - top: 2px; - opacity: 0; - transition: opacity 200ms ease-in-out; - display: flex; -` - -const IconContainer = styled.div` - width: 22px; - height: 24px; - display: flex; - align-items: center; - font-size: 16px; -` - -interface NavigatorItemProps { - label: string - iconPath?: string - depth: number - dotPlaceholder?: boolean - control?: React.ReactNode - visibleControl?: boolean - className?: string - folded?: boolean - active?: boolean - subtle?: boolean - onFoldButtonClick?: (event: React.MouseEvent) => void - onClick?: (event: React.MouseEvent) => void - onContextMenu?: (event: React.MouseEvent) => void - onDrop?: (event: React.DragEvent) => void - onDragOver?: (event: React.DragEvent) => void - onDragEnd?: (event: React.DragEvent) => void - onDoubleClick?: (event: React.MouseEvent) => void -} - -const NavigatorItem = ({ - label, - iconPath, - depth, - control, - visibleControl = false, - className, - folded, - // TODO: Delete dot placeholder style - dotPlaceholder, - active, - subtle, - onFoldButtonClick, - onClick, - onDoubleClick, - onContextMenu, - onDrop, - onDragOver, - onDragEnd, -}: NavigatorItemProps) => { - return ( - - {!dotPlaceholder && folded != null && ( - - - - )} - - {dotPlaceholder && ( -
    - -
    - )} - {iconPath != null && ( - - - - )} - - {control && {control}} -
    -
    - ) -} - -export default NavigatorItem diff --git a/src/components/atoms/NavigatorSeparator.tsx b/src/components/atoms/NavigatorSeparator.tsx deleted file mode 100644 index 810d58ad28..0000000000 --- a/src/components/atoms/NavigatorSeparator.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import styled from '../../lib/styled' -import { borderBottom } from '../../lib/styled/styleFunctions' - -const NavigatorSeparator = styled.div` - height: 1px; - margin: 10px 0; - box-sizing: border-box; - ${borderBottom}; -` - -export default NavigatorSeparator diff --git a/src/components/atoms/NoteDetailNavigatorItem.tsx b/src/components/atoms/NoteDetailNavigatorItem.tsx deleted file mode 100644 index 9ff165578a..0000000000 --- a/src/components/atoms/NoteDetailNavigatorItem.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import styled from '../../lib/styled' - -const NoteDetailNavigatorItem = styled.button` - background-color: transparent; - border: none; - display: flex; - white-space: nowrap; - align-items: center; - cursor: pointer; - overflow: hidden; - text-overflow: ellipsis; - transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.navItemColor}; - user-select: none; - &:hover { - color: ${({ theme }) => theme.navButtonHoverColor}; - } - - &:active, - &.active { - color: ${({ theme }) => theme.navButtonActiveColor}; - } -` - -export default NoteDetailNavigatorItem diff --git a/src/components/atoms/PageScrollableContent.tsx b/src/components/atoms/PageScrollableContent.tsx deleted file mode 100644 index fd806a6649..0000000000 --- a/src/components/atoms/PageScrollableContent.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import styled from '../../lib/styled' - -const PageScrollableContent = styled.div` - flex: 1; - overflow: auto; - padding: 1em; -` - -export default PageScrollableContent diff --git a/src/components/atoms/ProgressBar.tsx b/src/components/atoms/ProgressBar.tsx index e2dbbea8e6..d3c324fa0e 100644 --- a/src/components/atoms/ProgressBar.tsx +++ b/src/components/atoms/ProgressBar.tsx @@ -1,5 +1,5 @@ import React from 'react' -import styled from '../../lib/styled' +import styled from '../../shared/lib/styled' interface ProgressBarProps { progress: number @@ -11,7 +11,7 @@ const ProgressBar = ({ progress, className }: ProgressBarProps) => { } const ProgressBarStyled = styled.div` - border: 2px solid ${({ theme }) => theme.borderColor}; + border: 2px solid ${({ theme }) => theme.colors.border.main}; height: 30px; position: relative; width: 100%; @@ -23,7 +23,7 @@ const ProgressBarStyled = styled.div` position: absolute; top: 0; height: 100%; - background-color: ${({ theme }) => theme.primaryColor}; + background-color: ${({ theme }) => theme.colors.variants.primary.base}; width: ${({ progress }) => progress}%; } ` diff --git a/src/components/atoms/Spacer.tsx b/src/components/atoms/Spacer.tsx deleted file mode 100644 index b0a0ba8f9e..0000000000 --- a/src/components/atoms/Spacer.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import styled from '../../lib/styled' - -const Spacer = styled.div` - flex: 1; -` - -export default Spacer diff --git a/src/components/atoms/Spinner.tsx b/src/components/atoms/Spinner.tsx deleted file mode 100644 index 0308d8ae30..0000000000 --- a/src/components/atoms/Spinner.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { keyframes } from 'styled-components' -import styled from '../../lib/styled' - -const rotate = keyframes` - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -` - -const Spinner = styled.div` - border-style: solid; - border-color: ${({ theme }) => theme.primaryColor}; - border-right-color: transparent; - border-width: 2px; - width: 1em; - height: 1em; - display: inline-block; - border-radius: 50%; - animation: ${rotate} 0.75s linear infinite; -` - -export default Spinner diff --git a/src/components/atoms/StorageLayout.tsx b/src/components/atoms/StorageLayout.tsx deleted file mode 100644 index bedde20b21..0000000000 --- a/src/components/atoms/StorageLayout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { useCallback } from 'react' -import TwoPaneLayout from './TwoPaneLayout' -import { useGeneralStatus } from '../../lib/generalStatus' -import NoteStorageNavigator from '../organisms/NoteStorageNavigator' -import { NoteStorage } from '../../lib/db/types' - -interface StorageLayoutProps { - storage: NoteStorage - children: React.ReactNode -} - -const StorageLayout = ({ storage, children }: StorageLayoutProps) => { - const { generalStatus, setGeneralStatus } = useGeneralStatus() - const updateNavWidth = useCallback( - (leftWidth: number) => { - setGeneralStatus({ - sideBarWidth: leftWidth, - }) - }, - [setGeneralStatus] - ) - - return ( - } - right={children} - onResizeEnd={updateNavWidth} - /> - ) -} - -export default StorageLayout diff --git a/src/components/atoms/TagNavigatorListItem.tsx b/src/components/atoms/TagNavigatorListItem.tsx index 97628f419b..53109647a2 100644 --- a/src/components/atoms/TagNavigatorListItem.tsx +++ b/src/components/atoms/TagNavigatorListItem.tsx @@ -1,10 +1,7 @@ import React, { useCallback, useState } from 'react' -import Icon from './Icon' -import styled from '../../lib/styled' import { mdiClose } from '@mdi/js' import { flexCenter, - tagBackgroundColor, TagStyleProps, textOverflow, } from '../../lib/styled/styleFunctions' @@ -15,6 +12,9 @@ import DialogColorPicker from './dialog/DialogColorPicker' import { PopulatedTagDoc } from '../../lib/db/types' import { BaseTheme } from '../../lib/styled/BaseTheme' import { isColorBright } from '../../lib/colors' +import { tagBackgroundColor } from '../../shared/lib/styled/styleFunctions' +import styled from '../../shared/lib/styled' +import Icon from '../../shared/components/atoms/Icon' const TagItem = styled.li` border-radius: 4px; @@ -38,7 +38,7 @@ const TagItemAnchor = styled.button` ${textOverflow}; filter: invert( ${({ theme, color }) => - isColorBright((color as string) || theme.secondaryBackgroundColor) + isColorBright((color as string) || theme.colors.background.secondary) ? 100 : 0}% ); @@ -53,7 +53,7 @@ const TagRemoveButton = styled.button` color: #fff; filter: invert( ${({ theme, color }) => - isColorBright((color as string) || theme.secondaryBackgroundColor) + isColorBright((color as string) || theme.colors.background.secondary) ? 100 : 0}% ); diff --git a/src/components/atoms/TagNavigatorNewTagPopup.tsx b/src/components/atoms/TagNavigatorNewTagPopup.tsx index 1fd494fe3d..605c2fe402 100644 --- a/src/components/atoms/TagNavigatorNewTagPopup.tsx +++ b/src/components/atoms/TagNavigatorNewTagPopup.tsx @@ -6,50 +6,70 @@ import React, { useRef, useCallback, } from 'react' -import { - border, - backgroundColor, - borderColor, - contextMenuShadow, - uiTextColor, - activeBackgroundColor, - textOverflow, - inputStyle, -} from '../../lib/styled/styleFunctions' -import styled from '../../lib/styled' import { useEffectOnce } from 'react-use' import { mdiTag } from '@mdi/js' -import Icon from './Icon' import { isTagNameValid } from '../../lib/db/utils' import { useAnalytics, analyticsEvents } from '../../lib/analytics' import { PopulatedTagDoc } from '../../lib/db/types' +import styled from '../../shared/lib/styled' +import { + textColor, + textOverflow, + activeBackgroundColor, +} from '../../shared/lib/styled/styleFunctions' +import Icon from '../../shared/components/atoms/Icon' +import FormInput from '../../shared/components/molecules/Form/atoms/FormInput' +import cc from 'classcat' const Container = styled.div` - position: fixed; - background-color: white; - width: 200px; - max-height: 200px; - overflow-y: auto; + position: relative; + width: 100%; - ${backgroundColor} - ${border} - z-index: 9000; - ${backgroundColor} - ${borderColor} - ${contextMenuShadow} -` + .autocomplete__input { + line-height: inherit !important; + height: 28px !important; + width: 100%; + margin-top: 4px; + } -const TagNameInput = styled.input` - ${inputStyle}; - width: 100%; - height: 30px; - padding: 0 0.25em; + .autocomplete__container { + z-index: 9000; + position: absolute; + padding: ${({ theme }) => theme.sizes.spaces.xsm}px 0; + width: 100%; + height: auto; + max-height: 70vh; + border-radius: 4px; + display: flex; + flex-direction: column; + border: none; + left: 0; + top: 100%; + background-color: ${({ theme }) => theme.colors.background.primary}; + box-shadow: ${({ theme }) => theme.colors.shadow}; + } + + .autocomplete__option { + width: 100%; + padding: 0 ${({ theme }) => theme.sizes.spaces.xsm}px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: ${({ theme }) => theme.colors.text.subtle}; + text-decoration: none; + + &:hover, + &:focus { + background: ${({ theme }) => theme.colors.background.quaternary}; + color: ${({ theme }) => theme.colors.text.primary}; + } + } ` const MenuButton = styled.button` width: 100%; height: 30px; - ${uiTextColor}; + ${textColor}; background-color: transparent; border: none; display: flex; @@ -72,7 +92,6 @@ interface TagNavigatorNewTagPopupProps { storageTagMap: Map close: () => void appendTagByName: (tagName: string) => void - position: { top: number; right: number } } const TagNavigatorNewTagPopup = ({ @@ -80,7 +99,6 @@ const TagNavigatorNewTagPopup = ({ storageTagMap, close, appendTagByName, - position, }: TagNavigatorNewTagPopupProps) => { const [newTagName, setNewTagName] = useState('') const [menuIndex, setMenuIndex] = useState(0) @@ -202,56 +220,67 @@ const TagNavigatorNewTagPopup = ({ ) return ( - - + ) => { setMenuIndex(0) setNewTagName(event.target.value) }} onKeyDown={handleKeyInput} + autoComplete='off' /> - <> - {filteredStorageTags.map((storageTag, index) => ( - { - appendTagByName(storageTag.name) - setNewTagName('') - inputRef.current?.focus() - report(analyticsEvents.appendNoteTag) - }} - > - - {storageTag.name} - - ))} - + {filteredStorageTags.length > 0 && ( +
    + {filteredStorageTags.map((storageTag, index) => ( + { + appendTagByName(storageTag.name) + setNewTagName('') + inputRef.current?.focus() + report(analyticsEvents.appendNoteTag) + }} + > + + {storageTag.name} + + ))} +
    + )} + {!newTagNameIsEmpty && !tagSet.has(trimmedNewTagName) && !storageTagMap.has(trimmedNewTagName) && ( - { - appendTagByName(trimmedNewTagName) - setNewTagName('') - inputRef.current?.focus() - report(analyticsEvents.appendNoteTag) - report(analyticsEvents.addTag) - }} - > - Create  - - {newTagName} - +
    + { + appendTagByName(trimmedNewTagName) + setNewTagName('') + inputRef.current?.focus() + report(analyticsEvents.appendNoteTag) + report(analyticsEvents.addTag) + }} + > + Create  + + {newTagName} + +
    )} + {tags.includes(trimmedNewTagName) && ( diff --git a/src/components/atoms/ToolbarButton.tsx b/src/components/atoms/ToolbarButton.tsx deleted file mode 100644 index 5044c967db..0000000000 --- a/src/components/atoms/ToolbarButton.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { MouseEventHandler } from 'react' -import styled from '../../lib/styled' -import Icon from './Icon' -import { flexCenter, textOverflow } from '../../lib/styled/styleFunctions' -import cc from 'classcat' - -interface ToolbarButtonProps { - iconPath?: string - label?: string - title?: string - active?: boolean - onClick?: MouseEventHandler - limitWidth?: boolean -} - -const ToolbarButton = ({ - iconPath, - label, - title, - onClick, - active, - limitWidth, -}: ToolbarButtonProps) => { - return ( - - {iconPath != null && } - {label != null && label.length > 0 && ( -
    {label}
    - )} -
    - ) -} - -export default ToolbarButton - -const Container = styled.button` - height: 34px; - min-width: 28px; - - box-sizing: border-box; - outline: none; - - background-color: transparent; - ${flexCenter} - overflow: hidden; - - border: none; - cursor: pointer; - padding: 0 5px; - - & > .icon { - font-size: 18px; - flex-shrink: 0; - } - - & > .label { - font-size: 14px; - ${textOverflow} - } - & > .icon + .label { - margin-left: 2px; - } - - &.limitWidth > .label { - max-width: 50px; - } - - transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.navItemColor}; - &:hover { - color: ${({ theme }) => theme.navButtonHoverColor}; - } - - &:active, - &.active { - color: ${({ theme }) => theme.navButtonActiveColor}; - } -` diff --git a/src/components/atoms/ToolbarButtonGroup.tsx b/src/components/atoms/ToolbarButtonGroup.tsx deleted file mode 100644 index 0b02747e4a..0000000000 --- a/src/components/atoms/ToolbarButtonGroup.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import styled from '../../lib/styled' - -export default styled.div` - border-radius: 2px; - border: solid 1px ${({ theme }) => theme.colors.border}; - & > button { - margin: 0; - border: 0; - border-right: 1px solid ${({ theme }) => theme.colors.border}; - border-radius: 0; - &:first-child { - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; - } - &:last-child { - border-right: 0; - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - } - } -` diff --git a/src/components/atoms/ToolbarIconButton.tsx b/src/components/atoms/ToolbarIconButton.tsx deleted file mode 100644 index d845399901..0000000000 --- a/src/components/atoms/ToolbarIconButton.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react' -import styled from '../../lib/styled' -import Icon from './Icon' -import { flexCenter } from '../../lib/styled/styleFunctions' - -const Container = styled.button` - height: 32px; - width: 32px; - box-sizing: border-box; - font-size: 18px; - outline: none; - padding: 0 5px; - - background-color: transparent; - ${flexCenter} - - border: none; - border-radius: 3px; - cursor: pointer; - - transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.navItemColor}; - &:hover { - background-color: ${({ theme }) => theme.navItemHoverBackgroundColor}; - } - &:hover, - &:active, - &.active { - color: ${({ theme }) => theme.navButtonActiveColor}; - } -` - -interface ToolbarButtonProps { - iconPath: string - active?: boolean - title?: string - onContextMenu?: React.MouseEventHandler - onClick: React.MouseEventHandler -} - -const ToolbarIconButton = React.forwardRef( - ( - { - iconPath, - onClick, - onContextMenu, - active = false, - title, - }: ToolbarButtonProps, - ref - ) => ( - - - - ) -) - -export default ToolbarIconButton diff --git a/src/components/atoms/ToolbarSeparator.tsx b/src/components/atoms/ToolbarSeparator.tsx deleted file mode 100644 index 911f543fd7..0000000000 --- a/src/components/atoms/ToolbarSeparator.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import styled from '../../lib/styled' - -const ToolbarSeparator = styled.div` - width: 1px; - background-color: gray; - height: 24px; - margin: 0 12px; -` - -export default ToolbarSeparator diff --git a/src/components/atoms/ToolbarSlashSeparator.tsx b/src/components/atoms/ToolbarSlashSeparator.tsx deleted file mode 100644 index 01f2c90954..0000000000 --- a/src/components/atoms/ToolbarSlashSeparator.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react' -import styled from '../../lib/styled' -import { mdiSlashForward } from '@mdi/js' -import Icon from './Icon' -import { flexCenter } from '../../lib/styled/styleFunctions' - -const ToolbarSlashSeparator = () => { - return ( - - - - ) -} - -const Container = styled.div` - height: 24px; - margin: 0 -5px; - ${flexCenter} - color: ${({ theme }) => theme.navButtonColor}; - font-size: 14px; - user-select: none; -` - -export default ToolbarSlashSeparator diff --git a/src/components/atoms/TopbarSwitchSelector.tsx b/src/components/atoms/TopbarSwitchSelector.tsx deleted file mode 100644 index 9668fa4a36..0000000000 --- a/src/components/atoms/TopbarSwitchSelector.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { MouseEventHandler } from 'react' -import { border, borderRight } from '../../lib/styled/styleFunctions' -import styled from '../../lib/styled' - -interface TopbarSwitchSelectorItem { - active?: boolean - label: React.ReactNode - title: string - onClick?: MouseEventHandler -} - -interface TopbarSwitchSelectorProps { - onClick?: MouseEventHandler - onContextMenu?: MouseEventHandler - items: TopbarSwitchSelectorItem[] -} - -const TopbarSwitchSelector = ({ - onClick, - onContextMenu, - items, -}: TopbarSwitchSelectorProps) => { - return ( - - {items.map((item) => { - return ( - - {item.label} - - ) - })} - - ) -} - -export default TopbarSwitchSelector - -const Container = styled.div` - ${border} - height: 28px; - margin-left: 10px; - border-radius: 4px; - overflow: hidden; -` - -const ItemButton = styled.button` - background-color: transparent; - cursor: pointer; - height: 26px; - line-height: 26px; - padding: 0 15px; - transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.uiTextColor}; - border: none; - ${borderRight} - &:last-child { - border-right: none; - } - &.active { - color: ${({ theme }) => theme.primaryButtonLabelColor}; - background-color: ${({ theme }) => theme.primaryColor}; - } -` diff --git a/src/components/atoms/TwoPaneLayout.tsx b/src/components/atoms/TwoPaneLayout.tsx deleted file mode 100644 index a2c6d79173..0000000000 --- a/src/components/atoms/TwoPaneLayout.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React, { - useState, - useCallback, - useEffect, - useRef, - CSSProperties, -} from 'react' -import styled from '../../lib/styled' -import throttle from 'lodash/throttle' -import { clamp } from 'ramda' - -interface TwoPaneLayoutProps { - left: React.ReactNode - right: React.ReactNode - className?: string - style?: CSSProperties - defaultLeftWidth?: number - maxLeftWidth?: number - onResizeEnd?: (leftWidth: number) => void -} - -const minLeftWidth = 100 - -const Container = styled.div` - flex: 1px; - position: relative; - overflow: hidden; -` - -const Pane = styled.div` - position: absolute; - top: 0; - bottom: 0; - overflow: hidden; -` - -const DividerBorder = styled.div` - width: 1px; - height: 100%; - background-color: ${({ theme }) => theme.borderColor}; -` - -const DividerGraple = styled.div` - position: absolute; - top: 0; - bottom: 0; - border: 3px solid; - border-color: transparent; - box-sizing: content-box; - margin: -3px; - z-index: 100; - user-select: none; - cursor: col-resize; - &.active { - border-color: ${({ theme }) => theme.primaryColor}; - } -` - -interface DividerProps { - onMouseDown: React.MouseEventHandler - dragging: boolean - leftWidth: number -} - -const Divider = ({ onMouseDown, dragging, leftWidth }: DividerProps) => ( - - - -) - -const TwoPaneLayout = ({ - left, - right, - className, - style, - defaultLeftWidth = 250, - maxLeftWidth = 500, - onResizeEnd, -}: TwoPaneLayoutProps) => { - const [leftWidth, setLeftWidth] = useState(defaultLeftWidth) - const [dragging, setDragging] = useState(false) - const mouseupListenerIsSetRef = useRef(false) - const dragStartXPositionRef = useRef(0) - const previousLeftWidthRef = useRef(leftWidth) - - const startDragging = useCallback( - (event: React.MouseEvent) => { - event.preventDefault() - dragStartXPositionRef.current = event.clientX - previousLeftWidthRef.current = leftWidth - setDragging(true) - }, - [leftWidth] - ) - - const endDragging = useCallback((event: MouseEvent) => { - event.preventDefault() - setDragging(false) - }, []) - - const moveDragging = useCallback( - throttle((event: MouseEvent) => { - event.preventDefault() - const diff = event.clientX - dragStartXPositionRef.current - setLeftWidth( - clamp(minLeftWidth, maxLeftWidth, previousLeftWidthRef.current + diff) - ) - }, 1000 / 30), - [] - ) - - useEffect(() => { - if (dragging && !mouseupListenerIsSetRef.current) { - window.addEventListener('mouseup', endDragging) - window.addEventListener('mousemove', moveDragging) - mouseupListenerIsSetRef.current = true - return - } - - if (!dragging && mouseupListenerIsSetRef.current) { - window.removeEventListener('mouseup', endDragging) - window.removeEventListener('mousemove', moveDragging) - mouseupListenerIsSetRef.current = false - return - } - - return () => { - if (mouseupListenerIsSetRef.current) { - window.removeEventListener('mouseup', endDragging) - window.removeEventListener('mousemove', moveDragging) - } - } - }, [dragging, endDragging, moveDragging]) - - useEffect(() => { - if (onResizeEnd != null && !dragging) { - onResizeEnd(leftWidth) - } - }, [onResizeEnd, leftWidth, dragging]) - - return ( - - {left} - - {right} - - ) -} - -export default TwoPaneLayout diff --git a/src/components/atoms/dialog/DialogIcon.tsx b/src/components/atoms/dialog/DialogIcon.tsx deleted file mode 100644 index 63a7f3039b..0000000000 --- a/src/components/atoms/dialog/DialogIcon.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' -import { DialogIconTypes } from '../../../lib/dialog' -import styled from '../../../lib/styled' - -export const StyledIcon = styled.div` - font-size: 70px; - line-height: 100%; - margin-right: 15px; -` -type DialogIconProps = { - icon: DialogIconTypes -} - -const DialogIcon = ({ icon }: DialogIconProps) => ( - {getEmoji(icon)} -) - -export default DialogIcon - -function getEmoji(icon: DialogIconTypes): string { - switch (icon) { - case DialogIconTypes.Info: - return 'ℹ️' - case DialogIconTypes.Question: - return '❓' - case DialogIconTypes.Warning: - return '⚠️' - case DialogIconTypes.Error: - return '🚨' - default: - return '👻' - } -} diff --git a/src/components/atoms/form.tsx b/src/components/atoms/form.tsx index 6e7afb1be3..b61cc7ea11 100644 --- a/src/components/atoms/form.tsx +++ b/src/components/atoms/form.tsx @@ -1,11 +1,11 @@ import React from 'react' -import styled from '../../lib/styled' +import styled from '../../shared/lib/styled' import { border, + selectStyle, primaryButtonStyle, secondaryButtonStyle, -} from '../../lib/styled/styleFunctions' -import { selectStyle } from '../../lib/styled/styleFunctions' +} from '../../shared/lib/styled/styleFunctions' interface FormHeadingProps { depth?: number @@ -67,9 +67,8 @@ export const FormTextInput = styled.input` display: block; width: 100%; padding: 0.375rem 0.75rem; - line-height: 1.5; border-radius: 0.25rem; - ${border} + ${border}; background-color: white; &:disabled { color: gray; @@ -84,27 +83,20 @@ export const FormBlockquote = styled.blockquote<{ ${({ theme, variant }) => { switch (variant) { case 'danger': - return theme.dangerColor + return theme.colors.variants.danger.base case 'primary': default: - return theme.primaryColor + return theme.colors.variants.primary.base } }}; margin-left: 0; padding: 0.5em 1em; a { - color: ${({ theme }) => theme.primaryColor}; + color: ${({ theme }) => theme.colors.text.primary}; } ` -export const FormCheckInlineItemContainer = styled.div` - display: inline-flex; - align-items: center; - padding-left: 0; - margin-right: 0.75rem; -` - export const FormCheckInput = styled.input` margin-top: 0; margin-right: 0.3125rem; @@ -127,30 +119,6 @@ interface FormCheckItemProps { onChange?: React.ChangeEventHandler } -export const FormCheckInlineItem = ({ - children, - id, - type = 'checkbox', - checked, - className, - style, - disabled, - onChange, -}: FormCheckItemProps) => { - return ( - - - {children} - - ) -} - export const FormCheckItemContainer = styled.div` align-items: center; ` @@ -179,8 +147,6 @@ export const FormCheckItem = ({ ) } -export const FormCheckList = styled.div`` - export const FormPrimaryButton = styled.button` ${primaryButtonStyle}; padding: 0.375rem 0.75rem; @@ -208,8 +174,8 @@ export const FormSecondaryButton = styled.button` export const FormTransparentButton = styled.button` background-color: transparent; border: none; - color: ${({ theme }) => theme.navItemColor}; - background-color: ${({ theme }) => theme.navItemBackgroundColor}; + color: ${({ theme }) => theme.colors.text.primary}; + background-color: ${({ theme }) => theme.colors.background.primary}; padding: 0.375rem 0.75rem; font-size: 1rem; line-height: 1.5; @@ -221,8 +187,8 @@ export const FormTransparentButton = styled.button` } &:hover { - color: ${({ theme }) => theme.navItemActiveColor}; - background-color: ${({ theme }) => theme.navItemHoverBackgroundColor}; + color: ${({ theme }) => theme.colors.text.secondary}; + background-color: ${({ theme }) => theme.colors.background.secondary}; } ` @@ -235,19 +201,13 @@ export const FormSelect = styled.select` font-size: 14px; ` -export const FormField = styled.div` - padding: 1rem; - border-radius: 0.25rem; - ${border} -` - export const FormLabelGroup = styled.div` margin-bottom: 1rem; min-height: 32px; &:last-child { margin-bottom: 0; } - justify-content: flexend; + justify-content: flex-end; display: flex; align-items: center; ` diff --git a/src/components/atoms/markdown/CodeFence.tsx b/src/components/atoms/markdown/CodeFence.tsx index 06c7d9b31d..eb2b426d82 100644 --- a/src/components/atoms/markdown/CodeFence.tsx +++ b/src/components/atoms/markdown/CodeFence.tsx @@ -1,10 +1,10 @@ import React from 'react' -import styled from '../../../lib/styled' import copy from 'copy-to-clipboard' -import Icon from '../Icon' import { mdiContentCopy, mdiContentSave } from '@mdi/js' import { flexCenter } from '../../../lib/styled/styleFunctions' import { downloadBlob } from '../../../lib/download' +import styled from '../../../shared/lib/styled' +import Icon from '../../../shared/components/atoms/Icon' const CodeFenceContainer = styled.div` position: relative; @@ -27,14 +27,14 @@ const CodeFenceButton = styled.button` cursor: pointer; transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.navButtonColor}; + color: ${({ theme }) => theme.colors.text.primary}; &:hover { - color: ${({ theme }) => theme.navButtonHoverColor}; + color: ${({ theme }) => theme.colors.text.secondary}; } &:active, &.active { - color: ${({ theme }) => theme.navButtonActiveColor}; + color: ${({ theme }) => theme.colors.text.subtle}; } ` diff --git a/src/components/atoms/search/LocalSearchButton.tsx b/src/components/atoms/search/LocalSearchButton.tsx index 0ce890cf95..abe7613216 100644 --- a/src/components/atoms/search/LocalSearchButton.tsx +++ b/src/components/atoms/search/LocalSearchButton.tsx @@ -1,6 +1,6 @@ import React from 'react' -import styled from '../../../lib/styled/styled' -import Icon from '../Icon' +import styled from '../../../shared/lib/styled' +import Icon from '../../../shared/components/atoms/Icon' interface LocalSearchButtonProps { title?: string @@ -24,7 +24,7 @@ const LocalSearchButton = ({ className={className} onClick={onClick} > - + ) } @@ -46,14 +46,14 @@ const LocalSearchStyledButton = styled.button` border: none; transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.navItemColor}; + color: ${({ theme }) => theme.colors.text.primary}; &:hover { - color: ${({ theme }) => theme.navButtonHoverColor}; + color: ${({ theme }) => theme.colors.text.secondary}; } &:active, &.active { - background-color: ${({ theme }) => theme.secondaryButtonBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.tertiary}; color: #61a8e1; border-radius: 3px; } @@ -62,13 +62,13 @@ const LocalSearchStyledButton = styled.button` cursor: default; opacity: 0.5; &:hover { - color: ${({ theme }) => theme.navItemColor}; + color: ${({ theme }) => theme.colors.text.disabled}; } } &:focus { opacity: 0.6; - background-color: ${({ theme }) => theme.secondaryButtonBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.quaternary}; outline: 1px solid #61a8e1; border-radius: 3px; } diff --git a/src/components/atoms/search/SearchResultItem.tsx b/src/components/atoms/search/SearchResultItem.tsx index 9439e24fcd..9e410d9874 100644 --- a/src/components/atoms/search/SearchResultItem.tsx +++ b/src/components/atoms/search/SearchResultItem.tsx @@ -1,4 +1,4 @@ -import styled from '../../../lib/styled/styled' +import styled from '../../../shared/lib/styled' export const SearchResultItem = styled.div` display: flex; diff --git a/src/components/molecules/AppNavigatorBoostHubTeamItem.tsx b/src/components/molecules/AppNavigatorBoostHubTeamItem.tsx deleted file mode 100644 index 8fd553bde3..0000000000 --- a/src/components/molecules/AppNavigatorBoostHubTeamItem.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import React, { useCallback, useState, useRef } from 'react' -import { border, flexCenter } from '../../lib/styled/styleFunctions' -import styled from '../../lib/styled' -import { useRouter } from '../../lib/router' -import Icon from '../atoms/Icon' -import { mdiCloudOutline } from '@mdi/js' -import { osName } from '../../lib/platform' - -const Container = styled.div` - position: relative; - height: 48px; - width: 48px; - margin-bottom: 4px; - &:first-child { - margin-top: 10px; - } - ${flexCenter} - border-radius: 14px; - border-width: 3px; - border-style: solid; - border-color: transparent; - cursor: pointer; - &:hover { - border-color: ${({ theme }) => theme.borderColor}; - } - &.active { - border-color: ${({ theme }) => theme.textColor}; - } - - & > .teamIcon { - height: 20px; - width: 20px; - border-radius: 10px; - background-color: ${({ theme }) => theme.navBackgroundColor}; - ${border} - display: flex; - align-items: center; - justify-content: center; - position: absolute; - bottom: -5px; - right: -5px; - z-index: 1; - } - - & > .tooltip { - display: flex; - align-items: center; - position: fixed; - padding: 0 10px; - border-radius: 4px; - height: 36px; - z-index: 1000; - color: ${({ theme }) => theme.tooltipTextColor}; - background-color: ${({ theme }) => theme.tooltipBackgroundColor}; - user-select: none; - - .tooltip__icon { - margin-right: 5px; - } - .tooltip__name { - margin-right: 10px; - } - .tooltip__key { - } - } -` - -const MainButton = styled.button` - height: 36px; - width: 36px; - border-radius: 8px; - ${border} - cursor: pointer; - ${flexCenter} - font-size: 18px; - border: none; - background-color: ${({ theme }) => theme.teamSwitcherBackgroundColor}; - border: 1px solid ${({ theme }) => theme.teamSwitcherBorderColor}; - color: ${({ theme }) => theme.teamSwitcherTextColor}; - font-size: 13px; - & > .icon { - width: 36px; - height: 36px; - border-radius: 8px; - } - - &:hover, - &:active, - &.active { - cursor: pointer; - background-color: ${({ theme }) => theme.teamSwitcherHoverBackgroundColor}; - color: ${({ theme }) => theme.teamSwitcherHoverTextColor}; - } - - &:disabled { - opacity: 0.5; - cursor: default; - } -` - -interface AppNavigatorBoostHubTeamItemProps { - domain: string - name: string - active: boolean - iconUrl?: string - index: number -} - -interface Position { - top: number - left: number -} - -const AppNavigatorBoostHubTeamItem = ({ - active, - domain, - name, - iconUrl, - index, -}: AppNavigatorBoostHubTeamItemProps) => { - const { push } = useRouter() - const buttonRef = useRef(null) - const [tooltipPosition, setTooltipPosition] = useState(null) - - const showTooltip = useCallback(() => { - if (buttonRef.current == null) { - return - } - const rect = buttonRef.current.getBoundingClientRect() - setTooltipPosition({ - top: rect.top, - left: rect.left + rect.width + 10, - }) - }, []) - - const hideTooltip = useCallback(() => { - setTooltipPosition(null) - }, []) - - const navigateToTeam = useCallback(() => { - push(`/app/boosthub/teams/${domain}`) - }, [push, domain]) - - return ( - - - {iconUrl == null ? ( - name.slice(0, 1) - ) : ( - - )} - - - {tooltipPosition != null && ( -
    - - {name} - - {osName === 'macos' ? '⌘' : 'Ctrl'} {index + 1} - -
    - )} -
    - ) -} - -export default AppNavigatorBoostHubTeamItem diff --git a/src/components/molecules/AppNavigatorStorageItem.tsx b/src/components/molecules/AppNavigatorStorageItem.tsx deleted file mode 100644 index 927cc9f06e..0000000000 --- a/src/components/molecules/AppNavigatorStorageItem.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import React, { useCallback, useState, useRef } from 'react' -import { NoteStorage } from '../../lib/db/types' -import { flexCenter } from '../../lib/styled/styleFunctions' -import styled from '../../lib/styled' -import { useDb } from '../../lib/db' -import { useDialog, DialogIconTypes } from '../../lib/dialog' -import { useTranslation } from 'react-i18next' -import { MenuItemConstructorOptions } from 'electron' -import { openContextMenu } from '../../lib/electronOnly' -import { useStorageRouter } from '../../lib/storageRouter' -import { osName } from '../../cloud/lib/utils/platform' - -const Container = styled.div` - ${flexCenter} - position: relative; - height: 48px; - width: 48px; - margin-bottom: 4px; - border-radius: 14px; - border-width: 3px; - border-style: solid; - border-color: transparent; - - &:first-child { - margin-top: 10px; - } - &:hover { - border-color: ${({ theme }) => theme.borderColor}; - } - &.active { - border-color: ${({ theme }) => theme.textColor}; - } - - & > .tooltip { - display: flex; - align-items: center; - position: fixed; - padding: 0 10px; - border-radius: 4px; - height: 36px; - z-index: 1000; - color: ${({ theme }) => theme.tooltipTextColor}; - background-color: ${({ theme }) => theme.tooltipBackgroundColor}; - user-select: none; - - .tooltip__icon { - margin-right: 5px; - } - .tooltip__name { - margin-right: 10px; - } - .tooltip__key { - } - } -` - -const MainButton = styled.button` - ${flexCenter} - height: 36px; - width: 36px; - border-radius: 8px; - cursor: pointer; - font-size: 18px; - border: none; - background-color: ${({ theme }) => theme.teamSwitcherBackgroundColor}; - border: 1px solid ${({ theme }) => theme.teamSwitcherBorderColor}; - color: ${({ theme }) => theme.teamSwitcherTextColor}; - font-size: 13px; - - &:active, - &.active { - background-color: ${({ theme }) => theme.teamSwitcherHoverBackgroundColor}; - color: ${({ theme }) => theme.teamSwitcherHoverTextColor}; - cursor: pointer; - } - - &:disabled { - opacity: 0.5; - cursor: default; - } -` - -interface AppNavigatorStorageItemProps { - active: boolean - storage: NoteStorage - index: number -} - -interface Position { - top: number - left: number -} - -const AppNavigatorStorageItem = ({ - active, - storage, - index, -}: AppNavigatorStorageItemProps) => { - const { renameStorage, removeStorage } = useDb() - const { prompt, messageBox } = useDialog() - const { t } = useTranslation() - const { navigate } = useStorageRouter() - const buttonRef = useRef(null) - const [tooltipPosition, setTooltipPosition] = useState(null) - - const showTooltip = useCallback(() => { - if (buttonRef.current == null) { - return - } - const rect = buttonRef.current.getBoundingClientRect() - setTooltipPosition({ - top: rect.top, - left: rect.left + rect.width + 10, - }) - }, []) - - const hideTooltip = useCallback(() => { - setTooltipPosition(null) - }, []) - - const navigateToStorage = useCallback(() => { - navigate(storage.id) - }, [navigate, storage.id]) - - const openStorageContextMenu = useCallback( - (event: React.MouseEvent) => { - event.preventDefault() - event.stopPropagation() - const menuItems: MenuItemConstructorOptions[] = [ - { - type: 'normal', - label: t('storage.rename'), - click: async () => { - prompt({ - title: `Rename "${storage.name}" storage`, - message: t('storage.renameMessage'), - iconType: DialogIconTypes.Question, - defaultValue: storage.name, - submitButtonLabel: t('storage.rename'), - onClose: async (value: string | null) => { - if (value == null) return - await renameStorage(storage.id, value) - }, - }) - }, - }, - { type: 'separator' }, - { - type: 'normal', - label: t('storage.remove'), - click: async () => { - messageBox({ - title: `Remove "${storage.name}" storage`, - message: - storage.type === 'fs' - ? "This operation won't delete the actual storage folder. You can add it to the app again." - : t('storage.removeMessage'), - iconType: DialogIconTypes.Warning, - buttons: [t('storage.remove'), t('general.cancel')], - defaultButtonIndex: 0, - cancelButtonIndex: 1, - onClose: (value: number | null) => { - if (value === 0) { - removeStorage(storage.id) - } - }, - }) - }, - }, - ] - - openContextMenu({ menuItems }) - }, - [messageBox, prompt, renameStorage, removeStorage, storage, t] - ) - - return ( - - - {storage.name.slice(0, 1)} - - {tooltipPosition != null && ( -
    - {storage.name} - - {osName === 'macos' ? '⌘' : 'Ctrl'} {index + 1} - -
    - )} -
    - ) -} - -export default AppNavigatorStorageItem diff --git a/src/components/molecules/BookmarkNavigatorFragment.tsx b/src/components/molecules/BookmarkNavigatorFragment.tsx deleted file mode 100644 index c335d6c971..0000000000 --- a/src/components/molecules/BookmarkNavigatorFragment.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useCallback } from 'react' -import { NoteDoc, NoteStorage } from '../../lib/db/types' -import { useDb } from '../../lib/db' -import NavigatorItem from '../atoms/NavigatorItem' -import NavigatorButton from '../atoms/NavigatorButton' -import { mdiCardTextOutline, mdiClose, mdiStar } from '@mdi/js' -import { useRouter } from '../../lib/router' -import { useRouteParams } from '../../lib/routeParams' -import { useGeneralStatus } from '../../lib/generalStatus' -import { bookmarkItemId } from '../../lib/nav' - -interface BookmarkNavigatorFragmentProps { - storage: NoteStorage -} - -const BookmarkNavigatorFragment = ({ - storage, -}: BookmarkNavigatorFragmentProps) => { - const { unbookmarkNote } = useDb() - const { push } = useRouter() - const { sideNavOpenedItemSet, toggleSideNavOpenedItem } = useGeneralStatus() - const opened = sideNavOpenedItemSet.has(bookmarkItemId) - - const toggleBookmarks = useCallback(() => { - toggleSideNavOpenedItem(bookmarkItemId) - }, [toggleSideNavOpenedItem]) - - const params = useRouteParams() - const bookmarkedNoteList = storage.bookmarkedItemIds - .map((id) => { - return storage.noteMap[id] - }) - .filter((note) => note != null) as NoteDoc[] - - if (bookmarkedNoteList.length === 0) { - return null - } - - return ( - <> - - {opened && ( - <> - {bookmarkedNoteList.map((note) => { - const active = - params.name === 'storages.notes' && - params.storageId === storage.id && - params.noteId === note._id - const emptyTitle = note.title.trim().length === 0 - return ( - { - push( - note.folderPathname === '/' - ? `/app/storages/${storage.id}/notes/${note._id}` - : `/app/storages/${storage.id}/notes${note.folderPathname}/${note._id}` - ) - }} - control={ - { - unbookmarkNote(storage.id, note._id) - }} - /> - } - /> - ) - })} - - )} - - ) -} - -export default BookmarkNavigatorFragment diff --git a/src/components/molecules/EditorKeyMapSelect.tsx b/src/components/molecules/EditorKeyMapSelect.tsx index b23bd3e7a5..f35e8c270e 100644 --- a/src/components/molecules/EditorKeyMapSelect.tsx +++ b/src/components/molecules/EditorKeyMapSelect.tsx @@ -3,8 +3,8 @@ import { usePreferences } from '../../lib/preferences' import { openContextMenu } from '../../lib/electronOnly' import { MenuItemConstructorOptions } from 'electron' import BottomBarButton from '../atoms/BottomBarButton' -import Icon from '../atoms/Icon' import { mdiKeyboard } from '@mdi/js' +import Icon from '../../shared/components/atoms/Icon' const EditorKeyMapSelect = () => { const { preferences, setPreferences } = usePreferences() diff --git a/src/components/molecules/EditorSelectionStatus.tsx b/src/components/molecules/EditorSelectionStatus.tsx index b2a17f126d..2b738870d4 100644 --- a/src/components/molecules/EditorSelectionStatus.tsx +++ b/src/components/molecules/EditorSelectionStatus.tsx @@ -1,6 +1,6 @@ import React from 'react' import { EditorPosition } from '../../lib/CodeMirror' -import styled from '../../lib/styled' +import styled from '../../shared/lib/styled' interface EditorSelectionStatusProps { cursor: EditorPosition @@ -46,6 +46,6 @@ const Container = styled.div` padding: 0 5px; height: 24px; font-size: 14px; - color: ${({ theme }) => theme.uiTextColor}; + color: ${({ theme }) => theme.colors.text.primary}; user-select: none; ` diff --git a/src/components/molecules/EditorThemeSelect.tsx b/src/components/molecules/EditorThemeSelect.tsx index 967405a56f..155feba549 100644 --- a/src/components/molecules/EditorThemeSelect.tsx +++ b/src/components/molecules/EditorThemeSelect.tsx @@ -3,9 +3,9 @@ import { usePreferences } from '../../lib/preferences' import { openContextMenu } from '../../lib/electronOnly' import { MenuItemConstructorOptions } from 'electron' import BottomBarButton from '../atoms/BottomBarButton' -import Icon from '../atoms/Icon' import { mdiPaletteOutline } from '@mdi/js' import { themes } from '../../lib/CodeMirror' +import Icon from '../../shared/components/atoms/Icon' const EditorThemeSelect = () => { const { preferences, setPreferences } = usePreferences() diff --git a/src/components/molecules/FolderDetailListItem.tsx b/src/components/molecules/FolderDetailListItem.tsx index df7d68c8ab..43fa631c0f 100644 --- a/src/components/molecules/FolderDetailListItem.tsx +++ b/src/components/molecules/FolderDetailListItem.tsx @@ -1,15 +1,13 @@ import React, { ReactNode, MouseEventHandler } from 'react' -import styled from '../../lib/styled' -import { - borderBottom, - textOverflow, - flexCenter, -} from '../../lib/styled/styleFunctions' -import Icon from '../atoms/Icon' +import { textOverflow, flexCenter } from '../../lib/styled/styleFunctions' import cc from 'classcat' +import styled from '../../shared/lib/styled' +import { borderBottom } from '../../shared/lib/styled/styleFunctions' +import Icon, { IconSize } from '../../shared/components/atoms/Icon' interface FolderDetailListItemProps { iconPath?: string + iconSize?: IconSize label: string onClick?: MouseEventHandler meta?: ReactNode @@ -18,6 +16,7 @@ interface FolderDetailListItemProps { const FolderDetailListItem = ({ iconPath, + iconSize = 20, label, onClick, meta, @@ -27,7 +26,7 @@ const FolderDetailListItem = ({
    - {iconPath != null && } + {iconPath != null && }
    {label.trim().length === 0 ? 'Untitled' : label} @@ -47,7 +46,7 @@ const Container = styled.li` height: 40px; ${borderBottom} &:hover { - background-color: ${({ theme }) => theme.noteNavItemBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.secondary}; & > .control { display: flex; } @@ -64,20 +63,22 @@ const Container = styled.li` .icon { width: 40px; height: 40px; - ${flexCenter} + ${flexCenter}; + + color: ${({ theme }) => theme.colors.text.link}; } .label { flex: 1; ${textOverflow} &.subtle { - color: ${({ theme }) => theme.disabledUiTextColor}; + color: ${({ theme }) => theme.colors.text.subtle}; } } & > .control { display: none; } & > .meta { - color: ${({ theme }) => theme.disabledUiTextColor}; + color: ${({ theme }) => theme.colors.text.disabled}; ${textOverflow} } ` diff --git a/src/components/molecules/FolderDetailListNoteItem.tsx b/src/components/molecules/FolderDetailListNoteItem.tsx index 8c10c15023..13767c6522 100644 --- a/src/components/molecules/FolderDetailListNoteItem.tsx +++ b/src/components/molecules/FolderDetailListNoteItem.tsx @@ -1,11 +1,12 @@ import React, { useCallback, useMemo } from 'react' import FolderDetailListItem from './FolderDetailListItem' import { - mdiCardTextOutline, mdiTrashCanOutline, mdiStar, mdiStarOutline, - mdiRestore, + mdiFileDocumentOutline, + mdiArchive, + mdiFileUndoOutline, } from '@mdi/js' import { NoteDoc } from '../../lib/db/types' import { useRouter } from '../../lib/router' @@ -67,7 +68,7 @@ const FolderDetailListNoteItem = ({ return ( ) : ( <> diff --git a/src/components/molecules/FolderNavigatorFragment.tsx b/src/components/molecules/FolderNavigatorFragment.tsx deleted file mode 100644 index 6d5fa7d0d0..0000000000 --- a/src/components/molecules/FolderNavigatorFragment.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useMemo } from 'react' -import { NoteStorage } from '../../lib/db/types' -import { usePathnameWithoutNoteId } from '../../lib/routeParams' -import { useGeneralStatus } from '../../lib/generalStatus' -import { getFolderItemId } from '../../lib/nav' -import FolderNavigatorItem from './FolderNavigatorItem' - -interface FolderListFragmentProps { - storage: NoteStorage - createNoteInFolderAndRedirect: (folderPathname: string) => void - showPromptToCreateFolder: (folderPathname: string) => void - showPromptToRenameFolder: (folderPathname: string) => void -} - -const FolderListFragment = ({ - storage, - showPromptToCreateFolder, - showPromptToRenameFolder, - createNoteInFolderAndRedirect, -}: FolderListFragmentProps) => { - const { folderMap, id: storageId } = storage - - const { sideNavOpenedItemSet } = useGeneralStatus() - - const currentPathnameWithoutNoteId = usePathnameWithoutNoteId() - - const folderPathnames = useMemo(() => { - return Object.keys(folderMap).sort((a, b) => a.localeCompare(b)) - }, [folderMap]) - - const folderSetWithSubFolders = useMemo(() => { - return folderPathnames.reduce((folderSet, folderPathname) => { - if (folderPathname !== '/') { - const nameElements = folderPathname.slice(1).split('/') - const parentFolderPathname = - '/' + nameElements.slice(0, nameElements.length - 1).join('/') - folderSet.add(parentFolderPathname) - } - return folderSet - }, new Set()) - }, [folderPathnames]) - - const openedFolderPathnameList = useMemo(() => { - const tree = getFolderNameElementTree(folderPathnames) - return getOpenedFolderPathnameList( - tree, - storageId, - sideNavOpenedItemSet, - '/' - ) - }, [folderPathnames, storageId, sideNavOpenedItemSet]) - - return ( - <> - {openedFolderPathnameList.map((item) => { - return ( - - ) - })} - - ) -} - -function getFolderNameElementTree(folderPathnameList: string[]) { - return folderPathnameList.reduce((tree, folderPathname) => { - const nameElements = folderPathname.slice(1).split('/') - - let targetTree = tree - for (const nameElement of nameElements) { - if (targetTree[nameElement] == null) { - targetTree[nameElement] = {} - } - targetTree = targetTree[nameElement] - } - - return tree - }, {}) -} - -interface FolderNavItem { - name: string - pathname: string - depth: number -} - -function getOpenedFolderPathnameList( - tree: {}, - storageId: string, - openItemIdSet: Set, - parentPathname: string -): FolderNavItem[] { - const names = Object.keys(tree) - const pathnameList: FolderNavItem[] = [] - for (const name of names) { - const pathname = - parentPathname === '/' ? `/${name}` : `${parentPathname}/${name}` - if (pathname === '/') { - continue - } - const depth = pathname.split('/').slice(0).length - 2 - pathnameList.push({ - name, - pathname, - depth, - }) - if (openItemIdSet.has(getFolderItemId(storageId, pathname))) { - pathnameList.push( - ...getOpenedFolderPathnameList( - tree[name], - storageId, - openItemIdSet, - pathname - ) - ) - } - } - return pathnameList -} - -export default FolderListFragment diff --git a/src/components/molecules/FolderNoteNavigatorFragment.tsx b/src/components/molecules/FolderNoteNavigatorFragment.tsx deleted file mode 100644 index 5f28408e36..0000000000 --- a/src/components/molecules/FolderNoteNavigatorFragment.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import React, { useMemo } from 'react' -import { - NoteStorage, - NoteDoc, - PopulatedFolderDoc, - ObjectMap, -} from '../../lib/db/types' -import { useRouteParams, StorageNotesRouteParams } from '../../lib/routeParams' -import { useGeneralStatus } from '../../lib/generalStatus' -import { getFolderItemId } from '../../lib/nav' -import FolderNavigatorItem from './FolderNavigatorItem' -import NoteNavigatorItem from './NoteNavigatorItem' - -interface FolderNoteNavigatorFragment { - storage: NoteStorage - createNoteInFolderAndRedirect: (folderPathname: string) => void - showPromptToCreateFolder: (folderPathname: string) => void - showPromptToRenameFolder: (folderPathname: string) => void - bookmarkNote: ( - storageId: string, - noteId: string - ) => Promise - unbookmarkNote: ( - storageId: string, - noteId: string - ) => Promise - trashNote: (storageId: string, noteId: string) => Promise -} - -const FolderNoteNavigatorFragment = ({ - storage, - showPromptToCreateFolder, - showPromptToRenameFolder, - createNoteInFolderAndRedirect, - bookmarkNote, - unbookmarkNote, - trashNote, -}: FolderNoteNavigatorFragment) => { - const { folderMap, id: storageId } = storage - - const { sideNavOpenedItemSet } = useGeneralStatus() - - const routeParams = useRouteParams() as StorageNotesRouteParams - - const folderPathnames = useMemo(() => { - return Object.keys(folderMap).sort((a, b) => a.localeCompare(b)) - }, [folderMap]) - - const folderSetWithSubFolders = useMemo(() => { - return folderPathnames.reduce((folderSet, folderPathname) => { - if (folderPathname !== '/') { - const nameElements = folderPathname.slice(1).split('/') - const parentFolderPathname = - '/' + nameElements.slice(0, nameElements.length - 1).join('/') - folderSet.add(parentFolderPathname) - } - return folderSet - }, new Set()) - }, [folderPathnames]) - - const openedNavItemList = useMemo(() => { - const tree = getFolderNameElementTree(folderPathnames) - const navItemList = getOpenedFolderPathnameList( - tree, - storageId, - sideNavOpenedItemSet, - '/', - storage.folderMap, - storage.noteMap - ) - const rootFolderNoteIds = - storage.folderMap['/'] == null - ? [] - : [...storage.folderMap['/'].noteIdSet] || [] - const rootFolderNotes = rootFolderNoteIds.reduce((list, noteId) => { - const note = storage.noteMap[noteId] - if (note != null) { - list.push(note) - } - return list - }, [] as NoteDoc[]) - rootFolderNotes - .sort((a, b) => { - if (a.title.trim() === '' && b.title.trim() !== '') { - return 1 - } - if (b.title.trim() === '' && a.title.trim() !== '') { - return -1 - } - return a.title.trim().localeCompare(b.title.trim()) - }) - .forEach((note) => { - navItemList.push({ - type: 'note', - id: note._id, - title: note.title, - folderPathname: note.folderPathname, - bookmarked: !!note.data.bookmarked, - depth: 0, - }) - }) - return navItemList - }, [ - folderPathnames, - storageId, - sideNavOpenedItemSet, - storage.folderMap, - storage.noteMap, - ]) - - return ( - <> - {openedNavItemList.map((item) => { - if (item.type === 'folder') { - return ( - - ) - } - - return ( - - ) - })} - - ) -} - -function getFolderNameElementTree(folderPathnameList: string[]) { - return folderPathnameList.reduce((tree, folderPathname) => { - const nameElements = folderPathname.slice(1).split('/') - - let targetTree = tree - for (const nameElement of nameElements) { - if (targetTree[nameElement] == null) { - targetTree[nameElement] = {} - } - targetTree = targetTree[nameElement] - } - - return tree - }, {}) -} - -interface FolderNavItem { - type: 'folder' - name: string - pathname: string - noteCount: number - depth: number -} - -interface NoteNavItem { - type: 'note' - id: string - title: string - folderPathname: string - depth: number - bookmarked: boolean -} - -type NavItem = FolderNavItem | NoteNavItem - -function getOpenedFolderPathnameList( - tree: {}, - storageId: string, - openItemIdSet: Set, - parentPathname: string, - folderMap: ObjectMap, - noteMap: ObjectMap -): NavItem[] { - const names = Object.keys(tree) - const itemList: NavItem[] = [] - for (const name of names) { - const pathname = - parentPathname === '/' ? `/${name}` : `${parentPathname}/${name}` - if (pathname === '/') { - continue - } - const folderDoc = folderMap[pathname] - const noteCount = folderDoc?.noteIdSet.size || 0 - const nameElements = pathname.split('/').slice(1) - const depth = nameElements.length - 1 - itemList.push({ - type: 'folder', - pathname, - name, - depth, - noteCount, - }) - - const folderIsOpened = openItemIdSet.has( - getFolderItemId(storageId, pathname) - ) - if (folderIsOpened) { - itemList.push( - ...getOpenedFolderPathnameList( - tree[name], - storageId, - openItemIdSet, - pathname, - folderMap, - noteMap - ) - ) - - if (folderIsOpened && folderDoc != null) { - const noteIds = [...folderDoc.noteIdSet] - const folderNotes = noteIds.reduce((list, noteId) => { - const noteDoc = noteMap[noteId] - if (noteDoc != null) { - list.push(noteDoc) - } - return list - }, [] as NoteDoc[]) - folderNotes - .sort((a, b) => { - if (a.title.trim() === '' && b.title.trim() !== '') { - return 1 - } - if (b.title.trim() === '' && a.title.trim() !== '') { - return -1 - } - return a.title.trim().localeCompare(b.title.trim()) - }) - .forEach((noteDoc) => { - itemList.push({ - type: 'note', - id: noteDoc._id, - title: noteDoc.title, - folderPathname: noteDoc.folderPathname, - bookmarked: !!noteDoc.data.bookmarked, - depth: depth + 1, - }) - }) - } - } - } - - return itemList -} - -export default FolderNoteNavigatorFragment diff --git a/src/components/molecules/MessageBoxDialogBody.tsx b/src/components/molecules/MessageBoxDialogBody.tsx deleted file mode 100644 index e1d38c446e..0000000000 --- a/src/components/molecules/MessageBoxDialogBody.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { ChangeEventHandler, KeyboardEventHandler } from 'react' -import { MessageBoxDialogData } from '../../lib/dialog' -import { - DialogBodyContainer, - DialogTitle, - DialogMessage, - DialogButtonGroup, - DialogButton, -} from '../atoms/dialog/styled' - -type MessageBoxDialogProps = { - data: MessageBoxDialogData - closeDialog: () => void -} - -export default class MessageBoxDialogBody extends React.Component< - MessageBoxDialogProps -> { - defaultButtonRef = React.createRef() - - componentDidMount() { - this.defaultButtonRef.current!.focus() - } - - updateValue: ChangeEventHandler = (event) => { - this.setState({ - value: event.target.value, - }) - } - - handleBodyKeyDown: KeyboardEventHandler = (event) => { - const { data } = this.props - switch (event.key) { - case 'Escape': - if (data.cancelButtonIndex != null) { - this.close(data.cancelButtonIndex) - } - return - } - } - - close = (value: number) => () => { - const { data, closeDialog } = this.props - closeDialog() - data.onClose(value) - } - - render() { - const { data } = this.props - const { defaultButtonIndex = 0, title, message, buttons } = data - - return ( - - {title} - {message} - - {buttons.map((button, index) => ( - - {button} - - ))} - - - ) - } -} diff --git a/src/components/molecules/ModalContainer.tsx b/src/components/molecules/ModalContainer.tsx index 8eda25700a..c9d1211465 100644 --- a/src/components/molecules/ModalContainer.tsx +++ b/src/components/molecules/ModalContainer.tsx @@ -1,6 +1,6 @@ import React, { FC, MouseEventHandler } from 'react' -import { flexCenter } from '../../lib/styled/styleFunctions' -import styled from '../../lib/styled' +import styled from '../../shared/lib/styled' +import { flexCenter } from '../../shared/lib/styled/styleFunctions' interface ModalContainerProps { onShadowClick: MouseEventHandler diff --git a/src/components/molecules/NoteDetailTagNavigator.tsx b/src/components/molecules/NoteDetailTagNavigator.tsx index 4c9091bc8e..96b07dc675 100644 --- a/src/components/molecules/NoteDetailTagNavigator.tsx +++ b/src/components/molecules/NoteDetailTagNavigator.tsx @@ -1,19 +1,57 @@ import React, { useMemo, useState, useRef, useCallback, useEffect } from 'react' -import styled from '../../lib/styled' -import { mdiPlus } from '@mdi/js' import { useRouteParams } from '../../lib/routeParams' -import ToolbarIconButton from '../atoms/ToolbarIconButton' import TagNavigatorListItem from '../atoms/TagNavigatorListItem' import TagNavigatorNewTagPopup from '../atoms/TagNavigatorNewTagPopup' -import { useTranslation } from 'react-i18next' import { PopulatedTagDoc, NoteStorage } from '../../lib/db/types' import { entries } from '../../lib/db/utils' +import styled from '../../shared/lib/styled' +import { contextMenuFormItem } from '../../shared/lib/styled/styleFunctions' +import IconMdi from '../../cloud/components/atoms/IconMdi' +import { mdiPlus } from '@mdi/js' const Container = styled.div` display: flex; align-items: center; flex-wrap: wrap; position: relative; + + .tag__add--empty { + ${({ theme }) => contextMenuFormItem({ theme }, ':focus')}; + + font-size: ${({ theme }) => theme.sizes.fonts.df}px; + background-color: ${({ theme }) => theme.colors.background.secondary}; + outline: 0; + width: 100%; + display: block; + color: ${({ theme }) => theme.colors.text.subtle}; + height: 32px; + border-radius: 4px; + &:hover { + color: ${({ theme }) => theme.colors.text.primary}; + background-color: ${({ theme }) => theme.colors.background.quaternary}; + } + text-align: left; + } + + .tag__add { + font-size: ${({ theme }) => theme.sizes.fonts.df}px; + border-radius: 100%; + width: 25px; + height: 25px; + display: flex; + align-items: center; + justify-content: center; + background-color: ${({ theme }) => theme.colors.background.secondary}; + border: 1px solid ${({ theme }) => theme.colors.border.main}; + color: ${({ theme }) => theme.colors.text.subtle}; + margin: 5px 4px; + padding: 0; + + &:hover, + &:focus { + color: ${({ theme }) => theme.colors.text.primary} !important; + } + } ` const TagNavigatorList = styled.ul` @@ -22,7 +60,6 @@ const TagNavigatorList = styled.ul` padding: 0; margin: 0; flex-wrap: wrap; - overflow: hidden; gap: 5px; align-items: center; ` @@ -44,7 +81,6 @@ const NoteDetailTagNavigator = ({ removeTagByName, updateTagColorByName, }: NoteDetailTagNavigatorProps) => { - const { t } = useTranslation() const storageId = storage.id const storageTagMap = useMemo(() => { @@ -54,33 +90,26 @@ const NoteDetailTagNavigator = ({ const routeParams = useRouteParams() const currentTagName = useMemo(() => { - if (routeParams.name !== 'storages.tags.show') { + if (routeParams.name !== 'workspaces.labels.show') { return null } return routeParams.tagName }, [routeParams]) - const [newTagPopupPosition, setNewTagPopupPosition] = useState<{ - top: number - right: number - } | null>(null) + const [showingNewTagPopup, setShowingNewTagPopup] = useState(false) const buttonRef = useRef(null) const closeNewTagPopup = useCallback(() => { - setNewTagPopupPosition(null) + setShowingNewTagPopup(false) if (buttonRef.current == null) { return } buttonRef.current.focus() - }, [setNewTagPopupPosition]) + }, [setShowingNewTagPopup]) const showNewTagPopup = useCallback(() => { - const rect = buttonRef.current!.getBoundingClientRect() - setNewTagPopupPosition({ - right: 10, - top: rect.bottom, - }) - }, [setNewTagPopupPosition]) + setShowingNewTagPopup(true) + }, []) const noteTags = useMemo(() => { return tags @@ -97,7 +126,7 @@ const NoteDetailTagNavigator = ({ useEffect(() => { const resizeHandler = () => { - if (newTagPopupPosition == null) { + if (!showingNewTagPopup) { return } showNewTagPopup() @@ -106,7 +135,7 @@ const NoteDetailTagNavigator = ({ return () => { window.removeEventListener('resize', resizeHandler) } - }, [newTagPopupPosition, showNewTagPopup]) + }, [setShowingNewTagPopup, showNewTagPopup, showingNewTagPopup]) const appendTagByNameAndRefreshPopupPosition = useCallback( (tagName: string) => { @@ -134,21 +163,26 @@ const NoteDetailTagNavigator = ({ ) ) })} - - {newTagPopupPosition != null && ( + {showingNewTagPopup ? ( + ) : tags.length === 0 ? ( + + ) : ( + )} ) diff --git a/src/components/molecules/NoteItem.tsx b/src/components/molecules/NoteItem.tsx deleted file mode 100644 index 72f821fc56..0000000000 --- a/src/components/molecules/NoteItem.tsx +++ /dev/null @@ -1,347 +0,0 @@ -import React, { useMemo, useCallback } from 'react' -import styled from '../../lib/styled/styled' -import { - borderBottom, - uiTextColor, - secondaryBackgroundColor, - textOverflow, - TagStyleProps, -} from '../../lib/styled/styleFunctions' -import cc from 'classcat' -import { setTransferrableNoteData } from '../../lib/dnd' -import { formatDistanceToNow } from 'date-fns' -import { scaleAndTransformFromLeft } from '../../lib/styled' -import { useDb } from '../../lib/db' -import { useDialog, DialogIconTypes } from '../../lib/dialog' -import { useTranslation } from 'react-i18next' -import { NoteDoc, PopulatedTagDoc } from '../../lib/db/types' -import { useRouter } from '../../lib/router' -import { GeneralNoteListViewOptions } from '../../lib/preferences' -import { useGeneralStatus } from '../../lib/generalStatus' -import { bookmarkItemId } from '../../lib/nav' -import { openContextMenu } from '../../lib/electronOnly' -import { BaseTheme } from '../../lib/styled/BaseTheme' -import { isColorBright } from '../../lib/colors' - -const Container = styled.button` - margin: 0; - border-left: 2px solid transparent; - cursor: pointer; - width: 100%; - background-color: transparent; - text-align: left; - padding: 8px 10px 8px 8px; - ${uiTextColor}; - - border-color: transparent; - border-style: solid; - border-width: 0 0 0 2px; - - &.active, - &:active, - &:focus, - &:hover { - ${secondaryBackgroundColor} - } - &.active { - border-left-color: ${({ theme }) => theme.primaryColor}; - } - ${borderBottom}; - transition: 200ms background-color; - - &.new { - position: relative; - left: -200px; - transform: scaleY(0.3); - transform-origin: top left; - animation: ${scaleAndTransformFromLeft} 0.2s linear forwards; - } -` - -const TitleSection = styled.div` - font-size: 17px; - font-weight: bold; - width: 100%; - ${textOverflow} -` - -const DateSection = styled.div` - font-size: 10px; - margin-top: 5px; - font-style: italic; - ${textOverflow} -` - -const PreviewSection = styled.div` - margin-top: 6px; - ${textOverflow} -` - -const TagListSection = styled.div` - margin-top: 6px; - width: 100%; - overflow: hidden; - white-space: nowrap; - display: flex; -` - -const TagListItem = styled.div` - height: 20px; - padding: 0 8px; - margin-right: 2px; - border-radius: 10px; - background-color: ${({ theme, color }) => - color || theme.secondaryBackgroundColor}; - - font-size: 12px; - line-height: 20px; - ${textOverflow} -` - -const TagItemAnchor = styled.button` - background-color: transparent; - border: none; - text-decoration: none; - color: #fff; - filter: invert( - ${({ theme, color }) => - isColorBright((color as string) || theme.secondaryBackgroundColor) - ? 100 - : 0}% - ); -` - -type NoteItemProps = { - storageId: string - note: NoteDoc - noteTags: PopulatedTagDoc[] - active: boolean - recentlyCreated?: boolean - basePathname: string - focusList: () => void - noteListView: GeneralNoteListViewOptions - applyDefaultNoteListing: () => void - applyCompactListing: () => void -} - -const NoteItem = ({ - storageId, - note, - noteTags, - active, - basePathname, - recentlyCreated, - noteListView, - applyDefaultNoteListing, - applyCompactListing, -}: NoteItemProps) => { - const href = `${basePathname}/${note._id}` - const { - createNote, - trashNote, - purgeNote, - untrashNote, - bookmarkNote, - unbookmarkNote, - } = useDb() - const { push } = useRouter() - const { addSideNavOpenedItem } = useGeneralStatus() - - const { messageBox } = useDialog() - const { t } = useTranslation() - - const openUntrashedNoteContextMenu = useCallback( - (event: React.MouseEvent) => { - event.stopPropagation() - event.preventDefault() - - openContextMenu({ - menuItems: [ - { - type: 'normal', - label: 'Duplicate Note', - click: async () => { - createNote(storageId, { - title: note.title, - content: note.content, - folderPathname: note.folderPathname, - tags: note.tags, - data: note.data, - }) - }, - }, - { type: 'separator' }, - { - type: 'normal', - label: 'Trash Note', - click: async () => { - if (note.trashed) { - return - } - trashNote(storageId, note._id) - }, - }, - { type: 'separator' }, - !note.data.bookmarked - ? { - type: 'normal', - label: 'Bookmark', - click: () => { - bookmarkNote(storageId, note._id) - addSideNavOpenedItem(bookmarkItemId) - }, - } - : { - type: 'normal', - label: 'Unbookmark', - click: () => { - unbookmarkNote(storageId, note._id) - }, - }, - { type: 'separator' }, - { - type: 'normal', - label: 'Default View', - click: applyDefaultNoteListing, - }, - { - type: 'normal', - label: 'Compact View', - click: applyCompactListing, - }, - ], - }) - }, - [ - createNote, - storageId, - note.title, - note.content, - note.folderPathname, - note.tags, - note.data, - note.trashed, - note._id, - trashNote, - applyDefaultNoteListing, - applyCompactListing, - bookmarkNote, - unbookmarkNote, - addSideNavOpenedItem, - ] - ) - - const openTrashedNoteContextMenu = useCallback( - (event: React.MouseEvent) => { - event.stopPropagation() - event.preventDefault() - - openContextMenu({ - menuItems: [ - { - type: 'normal', - label: 'Restore Note', - click: async () => { - await untrashNote(storageId, note._id) - }, - }, - { type: 'separator' }, - { - type: 'normal', - label: 'Delete Note', - click: async () => { - messageBox({ - title: 'Delete Note', - message: t('note.deleteMessage'), - iconType: DialogIconTypes.Warning, - buttons: [t('note.delete2'), t('general.cancel')], - defaultButtonIndex: 0, - cancelButtonIndex: 1, - onClose: (value: number | null) => { - if (value === 0) { - purgeNote(storageId, note._id) - } - }, - }) - }, - }, - { type: 'separator' }, - { - type: 'normal', - label: 'Default View', - click: applyDefaultNoteListing, - }, - { - type: 'normal', - label: 'Compact View', - click: applyCompactListing, - }, - ], - }) - }, - [ - storageId, - note._id, - t, - untrashNote, - purgeNote, - messageBox, - applyDefaultNoteListing, - applyCompactListing, - ] - ) - - const contentPreview = useMemo(() => { - const trimmedContent = note.content.trim() - - return trimmedContent.split('\n').shift() || t('note.empty') - }, [note.content, t]) - - const loadTransferrableNoteData = useCallback( - (event: React.DragEvent) => { - setTransferrableNoteData(event, storageId, note) - }, - [storageId, note] - ) - - const navigateToNote = useCallback(() => { - push(href) - }, [push, href]) - - return ( - - - {note.title.length === 0 ? t('note.noTitle') : note.title} - - {noteListView !== 'compact' && ( - <> - - {formatDistanceToNow(new Date(note.updatedAt))} {t('note.date')} - - {contentPreview} - {noteTags.length > 0 && ( - - {noteTags.map((tag) => ( - - - {tag.name} - - - ))} - - )} - - )} - - ) -} - -export default NoteItem diff --git a/src/components/molecules/NoteNavigatorItem.tsx b/src/components/molecules/NoteNavigatorItem.tsx deleted file mode 100644 index c6d3f062b5..0000000000 --- a/src/components/molecules/NoteNavigatorItem.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useCallback, MouseEventHandler } from 'react' -import NavigatorItem from '../atoms/NavigatorItem' -import { mdiCardTextOutline, mdiDotsVertical } from '@mdi/js' -import { useStorageRouter } from '../../lib/storageRouter' -import { NoteDoc } from '../../lib/db/types' -import NavigatorButton from '../atoms/NavigatorButton' -import { openContextMenu } from '../../lib/electronOnly' - -interface NoteNavigatorItemProps { - storageId: string - noteId: string - noteTitle: string - noteFolderPath: string - noteBookmarked: boolean - active: boolean - depth: number - bookmarkNote: ( - storageId: string, - noteId: string - ) => Promise - unbookmarkNote: ( - storageId: string, - noteId: string - ) => Promise - trashNote: (storageId: string, noteId: string) => Promise -} - -const NoteNavigatorItem = ({ - storageId, - noteId, - noteTitle, - noteFolderPath, - active, - depth, - noteBookmarked, - bookmarkNote, - unbookmarkNote, - trashNote, -}: NoteNavigatorItemProps) => { - const emptyTitle = noteTitle.trim().length === 0 - const { navigateToNote } = useStorageRouter() - - const navigate = useCallback(() => { - navigateToNote(storageId, noteId, noteFolderPath) - }, [navigateToNote, storageId, noteId, noteFolderPath]) - - const openNoteContextMenu: MouseEventHandler = useCallback( - (event) => { - event.preventDefault() - openContextMenu({ - menuItems: [ - !noteBookmarked - ? { - label: 'Bookmark Note', - click: () => { - bookmarkNote(storageId, noteId) - }, - } - : { - label: 'Unbookmark Note', - click: () => { - unbookmarkNote(storageId, noteId) - }, - }, - { type: 'separator' }, - { - label: 'Trash Note', - click: () => { - trashNote(storageId, noteId) - }, - }, - ], - }) - }, - [storageId, noteId, noteBookmarked, bookmarkNote, unbookmarkNote, trashNote] - ) - - return ( - - } - /> - ) -} - -export default NoteNavigatorItem diff --git a/src/components/molecules/NotePageToolbarFolderHeader.tsx b/src/components/molecules/NotePageToolbarFolderHeader.tsx deleted file mode 100644 index 644ab6949e..0000000000 --- a/src/components/molecules/NotePageToolbarFolderHeader.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { useMemo } from 'react' -import { mdiBookOpen, mdiFolderOutline } from '@mdi/js' -import { useRouter } from '../../lib/router' -import ToolbarSlashSeparator from '../atoms/ToolbarSlashSeparator' -import ToolbarButton from '../atoms/ToolbarButton' - -interface NotePageToolbarFolderHeaderProps { - storageId: string - folderPathname: string -} - -interface FolderData { - name: string - pathname: string -} - -const NotePageToolbarFolderHeader = ({ - storageId, - folderPathname, -}: NotePageToolbarFolderHeaderProps) => { - const { push } = useRouter() - - const folderDataList = useMemo(() => { - if (folderPathname === '/') { - return [] - } - const folderNames = folderPathname.slice(1).split('/') - let pathname = '' - const folderDataList = [] - for (const folderName of folderNames) { - pathname += '/' + folderName - folderDataList.push({ - name: folderName, - pathname, - }) - } - return folderDataList - }, [folderPathname]) - - const navigateToWorkspace = () => { - push(`/app/storages/${storageId}/notes`) - } - return ( - <> - - {folderDataList.map(({ name, pathname }, index) => { - return ( - - - { - push(`/app/storages/${storageId}/notes${pathname}`) - }} - label={name} - limitWidth={index !== folderDataList.length - 1} - /> - - ) - })} - - ) -} - -export default NotePageToolbarFolderHeader diff --git a/src/components/molecules/NotePageToolbarNoteHeader.tsx b/src/components/molecules/NotePageToolbarNoteHeader.tsx deleted file mode 100644 index fc1fd1771c..0000000000 --- a/src/components/molecules/NotePageToolbarNoteHeader.tsx +++ /dev/null @@ -1,352 +0,0 @@ -import React, { - useMemo, - useState, - useCallback, - KeyboardEvent, - useRef, - useEffect, - MouseEventHandler, - FocusEventHandler, -} from 'react' -import { - mdiCardTextOutline, - mdiFolderOutline, - mdiPencilOutline, - mdiBookOpen, - mdiDotsHorizontal, -} from '@mdi/js' -import { useRouter } from '../../lib/router' -import ToolbarButton from '../atoms/ToolbarButton' -import ToolbarSlashSeparator from '../atoms/ToolbarSlashSeparator' -import { useDb } from '../../lib/db' -import styled from '../../lib/styled' -import { - inputStyle, - flexCenter, - textOverflow, - border, -} from '../../lib/styled/styleFunctions' -import { noteDetailFocusTitleInputEventEmitter } from '../../lib/events' -import { isChildNode } from '../../lib/dom' -import Icon from '../atoms/Icon' -import cc from 'classcat' -import FolderTreeListItem from '../atoms/FolderTreeListItem' -import { addIpcListener, removeIpcListener } from '../../lib/electronOnly' - -interface NotePageToolbarNoteHeaderProps { - storageId: string - storageName: string - noteId: string - noteFolderPathname: string - noteTitle: string -} - -interface FolderData { - name: string - pathname: string -} - -const NotePageToolbarNoteHeader = ({ - storageId, - noteId, - noteFolderPathname, - noteTitle, -}: NotePageToolbarNoteHeaderProps) => { - const { push } = useRouter() - const { updateNote } = useDb() - - const folderDataList = useMemo(() => { - if (noteFolderPathname === '/') { - return [] - } - const folderNames = noteFolderPathname.slice(1).split('/') - let pathname = '' - const folderDataList = [] - for (const folderName of folderNames) { - pathname += '/' + folderName - folderDataList.push({ - name: folderName, - pathname, - }) - } - return folderDataList - }, [noteFolderPathname]) - - const navigateToWorkspace = useCallback(() => { - push(`/app/storages/${storageId}/notes`) - }, [push, storageId]) - - const [editingTitle, setEditingTitle] = useState(false) - const [newTitle, setNewTitle] = useState(noteTitle) - const titleInputRef = useRef(null) - const startEditingTitle = useCallback(() => { - setNewTitle(noteTitle) - setEditingTitle(true) - }, [noteTitle]) - - useEffect(() => { - setNewTitle(noteTitle) - }, [noteTitle]) - - useEffect(() => { - if (editingTitle && titleInputRef.current != null) { - titleInputRef.current.focus() - } - }, [editingTitle]) - - const finishEditingTitle = useCallback(() => { - setEditingTitle(false) - updateNote(storageId, noteId, { title: newTitle }) - }, [storageId, noteId, newTitle, updateNote]) - - const stopEditingTitle = useCallback(() => { - setEditingTitle(false) - }, []) - - const updateNewTitle = useCallback((event) => { - setNewTitle(event.target.value) - }, []) - - const handleTitleInputKeyDown = useCallback( - (event: KeyboardEvent) => { - switch (event.key) { - case 'Enter': - event.preventDefault() - finishEditingTitle() - return - case 'Escape': - event.preventDefault() - stopEditingTitle() - } - }, - [finishEditingTitle, stopEditingTitle] - ) - - useEffect(() => { - noteDetailFocusTitleInputEventEmitter.listen(startEditingTitle) - return () => { - noteDetailFocusTitleInputEventEmitter.unlisten(startEditingTitle) - } - }, [startEditingTitle]) - - useEffect(() => { - addIpcListener('focus-title', startEditingTitle) - return () => { - removeIpcListener('focus-title', startEditingTitle) - } - }, [startEditingTitle]) - - const [ - showingParentFolderListPopup, - setShowingParentFolderPathnamePopup, - ] = useState(false) - const showParentFolderListPopup = useCallback(() => { - setShowingParentFolderPathnamePopup(true) - }, []) - const hideParentFolderListPopup: FocusEventHandler = useCallback( - (event) => { - if ( - parentFolderListPopupRef.current != null && - !isChildNode( - parentFolderListPopupRef.current, - event.relatedTarget as HTMLElement | null - ) - ) { - setShowingParentFolderPathnamePopup(false) - } - }, - [] - ) - const parentFolderListPopupRef = useRef(null) - - useEffect(() => { - if ( - showingParentFolderListPopup && - parentFolderListPopupRef.current != null - ) { - parentFolderListPopupRef.current.focus() - } - }, [showingParentFolderListPopup]) - - return ( - <> - - {folderDataList.length > 1 && ( - <> - - - {showingParentFolderListPopup && ( - -
      - {folderDataList - .slice(0, folderDataList.length - 1) - .map((folderData, index) => { - return ( - - ) - })} -
    -
    - )} - - )} - {folderDataList.length > 0 && ( - <> - - { - push( - `/app/storages/${storageId}/notes${ - folderDataList[folderDataList.length - 1].pathname - }` - ) - }} - /> - - )} - - - {editingTitle ? ( - - ) : ( - - )} - - ) -} - -export default NotePageToolbarNoteHeader - -const TitleInput = styled.input` - ${inputStyle} - flex: 1; - margin: 0 5px; - display: block; -` - -interface NoteTitleButtonProps { - title?: string - onClick: MouseEventHandler -} - -const NoteTitleButton = ({ title, onClick }: NoteTitleButtonProps) => { - const titleIsEmpty = title == null || title.trim().length === 0 - - return ( - - -
    - {titleIsEmpty ? 'Untitled' : title} -
    - -
    - ) -} - -const NoteTitleButtonContainer = styled.button` - height: 34px; - min-width: 28px; - - box-sizing: border-box; - outline: none; - - background-color: transparent; - ${flexCenter} - overflow: hidden; - - border: none; - cursor: pointer; - padding: 0 5px; - - & > .icon { - font-size: 18px; - flex-shrink: 0; - } - - & > .label { - font-size: 14px; - ${textOverflow} - &.empty { - color: ${({ theme }) => theme.disabledUiTextColor}; - } - } - - & > .icon + .label { - margin-left: 2px; - } - - & > .hoverIcon { - margin-left: 2px; - opacity: 0; - transition: opacity 200ms ease-in-out; - } - - transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.navItemColor}; - &:hover { - color: ${({ theme }) => theme.navButtonHoverColor}; - - & > .hoverIcon { - opacity: 1; - } - } - - &:active, - &.active { - color: ${({ theme }) => theme.navButtonActiveColor}; - & > .hoverIcon { - opacity: 1; - } - } -` - -const ParentFolderListPopup = styled.div` - position: fixed; - z-index: 1000; - max-height: 300px; - top: 30px; - ${({ theme }) => theme.shadow}; - background-color: ${({ theme }) => theme.backgroundColor}; - ${border}; - border-radius: 4px; - overflow-y: auto; - overflow-x: hidden; - & > ul { - padding: 0; - margin: 0; - list-style: none; - } -` diff --git a/src/components/molecules/PromptDialogBody.tsx b/src/components/molecules/PromptDialogBody.tsx deleted file mode 100644 index ba84551a9e..0000000000 --- a/src/components/molecules/PromptDialogBody.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React, { ChangeEventHandler, KeyboardEventHandler } from 'react' -import { PromptDialogOptions } from '../../lib/dialog' -import { - DialogBodyContainer, - DialogTitle, - DialogMessage, - DialogPromptInput, - DialogButtonGroup, - DialogButton, -} from '../atoms/dialog/styled' - -type PromptDialogProps = { - data: PromptDialogOptions - closeDialog: () => void -} - -type PromptDialogState = { - value: string -} - -export default class PromptDialogBody extends React.Component< - PromptDialogProps, - PromptDialogState -> { - state = { - value: - this.props.data.defaultValue == null ? '' : this.props.data.defaultValue, - } - inputRef = React.createRef() - - componentDidMount() { - this.inputRef.current!.focus() - } - - updateValue: ChangeEventHandler = (event) => { - this.setState({ - value: event.target.value, - }) - } - - handleBodyKeyDown: KeyboardEventHandler = (event) => { - switch (event.key) { - case 'Escape': - this.cancel() - return - } - } - - handleInputKeyDown: KeyboardEventHandler = (event) => { - switch (event.key) { - case 'Enter': - this.submit() - return - } - } - - submit = () => { - const { data, closeDialog } = this.props - closeDialog() - data.onClose(this.state.value) - } - - cancel = () => { - const { data, closeDialog } = this.props - closeDialog() - data.onClose(null) - } - - render() { - const { data } = this.props - return ( - - {data.title} - {data.message} - - - - {data.submitButtonLabel == null ? 'Submit' : data.submitButtonLabel} - - - {data.cancelButtonLabel == null ? 'Cancel' : data.cancelButtonLabel} - - - - ) - } -} diff --git a/src/components/molecules/SearchModalNoteResultItem.tsx b/src/components/molecules/SearchModalNoteResultItem.tsx index 82e27fadc8..f72b3a0271 100644 --- a/src/components/molecules/SearchModalNoteResultItem.tsx +++ b/src/components/molecules/SearchModalNoteResultItem.tsx @@ -1,18 +1,11 @@ import React, { useCallback, useMemo } from 'react' -import styled from '../../lib/styled' import { NoteDoc } from '../../lib/db/types' -import Icon from '../atoms/Icon' import { mdiCardTextOutline, mdiTagMultiple, mdiFolderOutline, mdiChevronRight, } from '@mdi/js' -import { - flexCenter, - borderBottom, - textOverflow, -} from '../../lib/styled/styleFunctions' import { getSearchResultKey, MAX_SEARCH_PREVIEW_LINE_LENGTH, @@ -22,6 +15,13 @@ import { import { SearchMatchHighlight } from '../PreferencesModal/styled' import { escapeRegExp } from '../../lib/string' import cc from 'classcat' +import styled from '../../shared/lib/styled' +import { + borderBottom, + flexCenter, + textOverflow, +} from '../../shared/lib/styled/styleFunctions' +import Icon from '../../shared/components/atoms/Icon' interface SearchModalNoteResultItemProps { note: NoteDoc @@ -238,10 +238,10 @@ const MetaContainer = styled.div` user-select: none; &:hover { - background-color: ${({ theme }) => theme.navItemHoverBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.tertiary}; } &:hover:active { - background-color: ${({ theme }) => theme.navItemHoverActiveBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.quaternary}; } & > .header { @@ -261,13 +261,14 @@ const MetaContainer = styled.div` font-size: 18px; ${textOverflow} &.empty { - color: ${({ theme }) => theme.disabledUiTextColor}; + color: ${({ theme }) => theme.colors.text.disabled}; } } } & > .meta { font-size: 12px; - color: ${({ theme }) => theme.navItemColor}; + //maybe white + color: ${({ theme }) => theme.colors.text.secondary}; display: flex; margin-left: 18px; @@ -313,18 +314,15 @@ const SearchResultItem = styled.div` margin-bottom: 2px; &.selected { - color: ${({ theme }) => theme.searchItemSelectionTextColor}; - background-color: ${({ theme }) => - theme.searchItemSelectionBackgroundColor}; + color: ${({ theme }) => theme.colors.text.primary}; + background-color: ${({ theme }) => theme.colors.background.quaternary}; } &.selected:hover { - background-color: ${({ theme }) => - theme.searchItemSelectionHoverBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.tertiary}; } &:hover { - background-color: ${({ theme }) => - theme.secondaryButtonHoverBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.secondary}; } ` @@ -337,7 +335,7 @@ const SearchResultLeft = styled.div` const SearchResultRight = styled.div` flex-shrink: 0; - ${flexCenter} + ${flexCenter}; & > .button { height: 24px; @@ -349,14 +347,14 @@ const SearchResultRight = styled.div` border: none; transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.navItemColor}; + color: ${({ theme }) => theme.colors.text.primary}; &:hover { - color: ${({ theme }) => theme.navButtonHoverColor}; + color: ${({ theme }) => theme.colors.text.secondary}; } &:active, &.active { - color: ${({ theme }) => theme.navButtonActiveColor}; + color: ${({ theme }) => theme.colors.text.link}; } } ` diff --git a/src/components/molecules/TagListFragment.tsx b/src/components/molecules/TagListFragment.tsx deleted file mode 100644 index e24370108f..0000000000 --- a/src/components/molecules/TagListFragment.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useMemo, useCallback } from 'react' -import SideNavigatorItem from '../atoms/NavigatorItem' -import NavigatorButton from '../atoms/NavigatorButton' -import { NoteStorage } from '../../lib/db/types' -import { isTagNameValid, keys } from '../../lib/db/utils' -import { useGeneralStatus } from '../../lib/generalStatus' -import { getTagListItemId } from '../../lib/nav' -import { useRouter } from '../../lib/router' -import { usePathnameWithoutNoteId } from '../../lib/routeParams' -import { useDialog, DialogIconTypes } from '../../lib/dialog' -import { useDb } from '../../lib/db' -import { useTranslation } from 'react-i18next' -import { mdiTag, mdiTagMultiple, mdiDotsVertical } from '@mdi/js' -import { openContextMenu } from '../../lib/electronOnly' -import { useAnalytics, analyticsEvents } from '../../lib/analytics' - -interface TagListFragmentProps { - storage: NoteStorage -} - -const TagListFragment = ({ storage }: TagListFragmentProps) => { - const { toggleSideNavOpenedItem, sideNavOpenedItemSet } = useGeneralStatus() - const { id: storageId, tagMap } = storage - const { t } = useTranslation() - - const tagListNavItemId = getTagListItemId(storage.id) - const tagListIsFolded = !sideNavOpenedItemSet.has(tagListNavItemId) - - const tagList = useMemo(() => { - return keys(tagMap).map((tagName) => { - return ( - - ) - }) - }, [storageId, tagMap]) - - if (tagList.length === 0) { - return null - } - - return ( - <> - 0 ? tagListIsFolded : undefined} - onFoldButtonClick={() => { - toggleSideNavOpenedItem(tagListNavItemId) - }} - onClick={() => { - toggleSideNavOpenedItem(tagListNavItemId) - }} - onContextMenu={(event) => { - event.preventDefault() - }} - /> - {!tagListIsFolded && tagList} - - ) -} - -export default TagListFragment - -interface TagListItemProps { - storageId: string - tagName: string -} - -const TagListItem = ({ tagName, storageId }: TagListItemProps) => { - const { push } = useRouter() - const { prompt, messageBox } = useDialog() - const { removeTag, renameTag } = useDb() - const { t } = useTranslation() - const currentPathname = usePathnameWithoutNoteId() - const { report } = useAnalytics() - const openTagContextMenu = useCallback( - (event: React.MouseEvent) => { - event.preventDefault() - event.stopPropagation() - openContextMenu({ - menuItems: [ - { - type: 'normal', - label: t('tag.rename'), - click: () => { - prompt({ - title: `tag.rename`, - message: t('tag.renameMessage', { tagName }), - iconType: DialogIconTypes.Question, - defaultValue: tagName, - submitButtonLabel: t('tag.rename'), - onClose: (value: string | null) => { - if ( - value == null || - !isTagNameValid(value) || - value == tagName - ) - return - renameTag(storageId, tagName, value) - report(analyticsEvents.renameTag) - }, - }) - }, - }, - { type: 'separator' }, - { - type: 'normal', - label: t('tag.remove'), - click: () => { - messageBox({ - title: `Remove "${tagName}" tag`, - message: t('tag.removeMessage'), - iconType: DialogIconTypes.Warning, - buttons: [t('tag.remove'), t('general.cancel')], - defaultButtonIndex: 0, - cancelButtonIndex: 1, - onClose: (value: number | null) => { - if (value === 0) { - removeTag(storageId, tagName) - report(analyticsEvents.removeTag) - } - }, - }) - }, - }, - ], - }) - }, - [messageBox, prompt, removeTag, renameTag, report, storageId, t, tagName] - ) - const tagPathname = `/app/storages/${storageId}/tags/${tagName}` - const tagIsActive = currentPathname === tagPathname - return ( - { - push(tagPathname) - }} - active={tagIsActive} - onContextMenu={openTagContextMenu} - control={ - - } - /> - ) -} diff --git a/src/components/molecules/Timeline/TimelineList.tsx b/src/components/molecules/Timeline/TimelineList.tsx new file mode 100644 index 0000000000..204e11f4d6 --- /dev/null +++ b/src/components/molecules/Timeline/TimelineList.tsx @@ -0,0 +1,148 @@ +import React, { useMemo } from 'react' +import styled from '../../../shared/lib/styled' +import { NoteDoc, NoteStorage } from '../../../lib/db/types' +import { TimelineEvent } from '../../pages/TimelinePage' +import { useUpDownNavigationListener } from '../../../shared/lib/keyboard' +import TimelineListItem from './TimelineListItem' +import { getHexFromUUID } from '../../../cloud/lib/utils/string' + +export interface TimelineListProps { + heading: string + events: TimelineEvent[] + storage: NoteStorage + timeFormat?: (date: Date) => string +} + +const TimelineList = ({ heading, events, storage }: TimelineListProps) => { + const listRef = React.createRef() + useUpDownNavigationListener(listRef) + + const timelineDocs = useMemo(() => { + return events.reduce< + { + doc: NoteDoc + }[] + >((acc, event) => { + if ( + event.type === 'createDoc' || + event.type === 'contentUpdate' || + event.type === 'archiveDoc' + ) { + const doc = storage.noteMap[event.id] + if (doc != null) { + acc.push({ doc }) + } + } + return acc + }, []) + }, [events, storage.noteMap]) + + if (events.length === 0) { + return null + } + + return ( + +

    {heading}

    + + {timelineDocs.length > 0 ? ( + + {timelineDocs.map((timelineDoc) => { + const childId = `timelinePage-dC${getHexFromUUID( + timelineDoc.doc._id + )}` + + return ( + + ) + })} + + ) : ( +

    No documents or folders have been updated/archived.

    + )} +
    +
    + ) +} + +export default TimelineList + +const StyledTimelineList = styled.div` + svg { + font-size: ${({ theme }) => theme.sizes.fonts.xl}px; + vertical-align: middle !important; + } + + .controls svg { + transform: translateY(0) !important; + } + + .label { + padding-left: ${({ theme }) => theme.sizes.spaces.xsm}px; + margin-bottom: 0; + } +` + +const StyledTimelineListContentWrapper = styled.div` + width: 100%; + margin: auto; + + p { + color: ${({ theme }) => theme.colors.text.primary}; + font-size: ${({ theme }) => theme.sizes.fonts.sm}px; + } +` + +const StyledTimelineListContent = styled.div` + .marginLeft { + margin-left: 0 !important; + } + + .sideNavItemStyle { + border-bottom: 1px solid ${({ theme }) => theme.colors.border.second}; + padding: ${({ theme }) => theme.sizes.spaces.xsm}px 0px; + align-items: flex-start; + justify-content: initial; + flex-direction: column; + height: auto; + + &:not(.non-hover):hover { + background-color: ${({ theme }) => theme.colors.background.tertiary}; + } + + &:not(.non-hover):focus, + &:not(.non-hover):active, + &:not(.non-hover).active { + background-color: ${({ theme }) => theme.colors.background.quaternary}; + } + + &:not(.non-hover).focused { + background-color: transparent; + } + } + + .sideNavWrapper { + width: 100%; + flex: inherit; + padding: 0 ${({ theme }) => theme.sizes.spaces.xsm}px; + z-index: initial !important; + } + + .controls { + padding-left: 0; + } + + .itemLink { + padding: 0 ${({ theme }) => theme.sizes.spaces.xsm}px; + + > div { + font-size: ${({ theme }) => theme.sizes.fonts.df}px; + white-space: inherit; + } + } +` diff --git a/src/components/molecules/Timeline/TimelineListItem.tsx b/src/components/molecules/Timeline/TimelineListItem.tsx new file mode 100644 index 0000000000..fa19801b43 --- /dev/null +++ b/src/components/molecules/Timeline/TimelineListItem.tsx @@ -0,0 +1,194 @@ +import React, { useCallback, useMemo, useState } from 'react' +import { NoteDoc, NoteStorage } from '../../../lib/db/types' +import cc from 'classcat' +import { mdiFileDocumentOutline } from '@mdi/js' +import { getFormattedBoosthubDate } from '../../../cloud/lib/date' +import Icon from '../../../shared/components/atoms/Icon' +import { getNoteTitle } from '../../../lib/db/utils' +import { useStorageRouter } from '../../../lib/storageRouter' +import { + SideNavItemStyle, + SideNavIconStyle, + SideNavLabelStyle, + StyledNavTagsList, + SideNavClickableButtonStyle, + SideNavControlStyle, + StyledTag, + sidebarText, +} from '../../../lib/styled/styleFunctionsLocal' +import styled from '../../../shared/lib/styled' +import { defaultTagColor } from '../../../lib/colors' + +interface TimelineListItemProps { + className?: string + item: NoteDoc + id: string + storage: NoteStorage +} + +const TimelineListItem = ({ + className, + item, + id, + storage, +}: TimelineListItemProps) => { + const [focused, setFocused] = useState(false) + + const onBlurHandler = (event: any) => { + if ( + document.activeElement == null || + !event.currentTarget.contains(event.relatedTarget) + ) { + setFocused(false) + } + } + + const dateLabel = useMemo(() => { + if (item.archivedAt != null) { + return ( +
    + Archived {getFormattedBoosthubDate(item.archivedAt, true)} +
    + ) + } + + return ( +
    + Updated {getFormattedBoosthubDate(item.updatedAt, true)} +
    + ) + }, [item.archivedAt, item.updatedAt]) + + const { navigateToNote: _navigateToNote } = useStorageRouter() + + const navigateToNote = useCallback( + (note: NoteDoc) => { + _navigateToNote(storage.id, note._id, note.folderPathname) + }, + [_navigateToNote, storage.id] + ) + + const noteTags = useCallback( + (doc: NoteDoc) => { + return doc.tags + .slice(0, 3) + .sort() + .reduce((list: TagItemProps[], tagName: string) => { + const tagDoc = storage.tagMap[tagName] + if (tagDoc != null) { + list.push({ + id: tagDoc._id, + color: + typeof tagDoc.data.color == 'string' + ? tagDoc.data.color + : defaultTagColor, + name: tagName, + }) + } + return list + }, []) as TagItemProps[] + }, + [storage.tagMap] + ) + + // const getTagDynamicStyle = useCallback((tag: TagStyleProps) => { + // const invertPercentage = isColorBright(tag.color) ? '100%' : '0%' + // return { + // color: '#fff', + // filter: `invert(${invertPercentage})`, + // } + // }, []) + + return ( + +
    + + + + + + + + { + navigateToNote(item) + }} + > + + {getNoteTitle(item, 'Untitled')} + + {item.tags != null && item.tags.length > 0 && ( + + {/*
    */} + {/* {noteTags(item).map((tag: TagItemProps) => (*/} + {/* */} + {/* {tag.name}*/} + {/* */} + {/* ))}*/} + {/* {item.tags.length > 3 && (*/} + {/* */} + {/* +{item.tags.length - 3}*/} + {/* */} + {/* )}*/} + {/*
    */} +
    + {noteTags(item).map((tag: TagItemProps) => ( + + {tag.name} + + ))} + {item.tags.length > 3 && ( + + +{item.tags.length - 3} + + )} +
    +
    + )} +
    +
    + + {dateLabel} + +
    +
    + ) +} + +interface TagItemProps { + id: string + name: string + color: string +} + +const TimelineIconColorStyle = styled.div` + color: ${({ theme }) => theme.colors.text.link}; +` + +const DocLinkStyle = styled.div` + padding: 0; + background-color: transparent; + border: none; + min-width: 0; + width: 100%; + flex: 1 1 auto; + font-size: ${({ theme }) => theme.sizes.fonts.df}px; + cursor: pointer; + ${sidebarText} + + .icon { + flex: 0 0 auto; + color: ${({ theme }) => theme.colors.text.subtle}; + } +` + +export default TimelineListItem diff --git a/src/components/organisms/AppNavigator.tsx b/src/components/organisms/AppNavigator.tsx index 25079b16f7..4eeba093ec 100644 --- a/src/components/organisms/AppNavigator.tsx +++ b/src/components/organisms/AppNavigator.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import styled from '../../lib/styled' import { useDb } from '../../lib/db' import { entries } from '../../lib/db/utils' @@ -15,6 +15,7 @@ import { mdiMenu, mdiCloudOffOutline, mdiGiftOutline, + mdiMessageQuestion, } from '@mdi/js' import { useRouter } from '../../lib/router' import { useActiveStorageId, useRouteParams } from '../../lib/routeParams' @@ -32,7 +33,6 @@ import SidebarSpaces, { } from '../../shared/components/organisms/Sidebar/molecules/SidebarSpaces' import { useStorageRouter } from '../../lib/storageRouter' import { MenuItemConstructorOptions } from 'electron/main' -import { DialogIconTypes, useDialog } from '../../lib/dialog' import { useTranslation } from 'react-i18next' import RoundedImage from '../../shared/components/atoms/RoundedImage' import { values } from 'ramda' @@ -52,18 +52,23 @@ import { SidebarState } from '../../shared/lib/sidebar' import CloudIntroModal from './CloudIntroModal' import { useCloudIntroModal } from '../../lib/cloudIntroModal' import { isEligibleForDiscount } from '../../cloud/lib/subscription' +import { DialogIconTypes, useDialog } from '../../shared/lib/stores/dialog' +import BasicInputFormLocal from '../v2/organisms/BasicInputFormLocal' +import { useModal } from '../../shared/lib/stores/modal' +import { useToast } from '../../shared/lib/stores/toast' const TopLevelNavigator = () => { const { storageMap, renameStorage, removeStorage } = useDb() const { push } = useRouter() - const { preferences, togglePreferencesModal, closed } = usePreferences() + const { preferences, togglePreferencesModal } = usePreferences() const { generalStatus } = useGeneralStatus() const routeParams = useRouteParams() const { signOut } = useBoostHub() const { navigate } = useStorageRouter() - const { prompt, messageBox } = useDialog() + const { messageBox } = useDialog() + const { openModal, closeLastModal } = useModal() + const { pushMessage } = useToast() const { t } = useTranslation() - const { toggleShowSearchModal, showSearchModal } = useSearchModal() const [sidebarState, setSidebarState] = useState( 'tree' ) @@ -145,15 +150,15 @@ const TopLevelNavigator = () => { const spaces = useMemo(() => { const spaces: SidebarSpace[] = [] - entries(storageMap).forEach(([storageId, storage], index) => { + entries(storageMap).forEach(([workspaceId, workspace], index) => { spaces.push({ - label: storage.name, - active: activeStorageId === storageId, + label: workspace.name, + active: activeStorageId === workspaceId, tooltip: `${osName === 'macos' ? '⌘' : 'Ctrl'} ${index + 1}`, linkProps: { onClick: (event) => { event.preventDefault() - navigate(storage.id) + navigate(workspace.id) }, onContextMenu: (event) => { event.preventDefault() @@ -163,17 +168,32 @@ const TopLevelNavigator = () => { type: 'normal', label: t('storage.rename'), click: async () => { - prompt({ - title: `Rename "${storage.name}" storage`, - message: t('storage.renameMessage'), - iconType: DialogIconTypes.Question, - defaultValue: storage.name, - submitButtonLabel: t('storage.rename'), - onClose: async (value: string | null) => { - if (value == null) return - await renameStorage(storage.id, value) - }, - }) + openModal( + { + if (workspaceName == '' || workspaceName == null) { + pushMessage({ + title: 'Cannot rename workspace', + description: 'Workspace name should not be empty.', + }) + return + } + await renameStorage(workspace.id, workspaceName) + closeLastModal() + }} + />, + { + showCloseIcon: true, + title: `Rename "${workspace.name}" storage`, + } + ) }, }, { type: 'separator' }, @@ -181,21 +201,24 @@ const TopLevelNavigator = () => { type: 'normal', label: t('storage.remove'), click: async () => { + // todo: [komediruzecki-22/05/2021] Test storage remove, message, action and errors messageBox({ - title: `Remove "${storage.name}" storage`, + title: `Remove "${workspace.name}" storage`, message: - storage.type === 'fs' + workspace.type === 'fs' ? "This operation won't delete the actual storage folder. You can add it to the app again." : t('storage.removeMessage'), iconType: DialogIconTypes.Warning, - buttons: [t('storage.remove'), t('general.cancel')], - defaultButtonIndex: 0, - cancelButtonIndex: 1, - onClose: (value: number | null) => { - if (value === 0) { - removeStorage(storage.id) - } - }, + buttons: [ + { + label: t('storage.remove'), + defaultButton: true, + onClick: () => { + removeStorage(workspace.id) + }, + }, + { label: t('general.cancel'), cancelButton: true }, + ], }) }, }, @@ -226,19 +249,27 @@ const TopLevelNavigator = () => { return spaces }, [ - activeStorageId, storageMap, - activeBoostHubTeamDomain, generalStatus.boostHubTeams, + activeStorageId, + navigate, + t, + openModal, + renameStorage, + closeLastModal, + pushMessage, messageBox, - prompt, removeStorage, - renameStorage, - navigate, + activeBoostHubTeamDomain, push, - t, ]) + const openState = useCallback((state: SidebarState) => { + setSidebarState((prev) => (prev === state ? undefined : state)) + }, []) + + const { showSearchModal, toggleShowSearchModal } = useSearchModal() + const toolbarRows = useMemo(() => { const boosthubTeam = activeBoostHubTeamDomain != null @@ -347,6 +378,12 @@ const TopLevelNavigator = () => { icon: mdiMagnify, onClick: toggleShowSearchModal, }, + { + tooltip: 'Timeline', + active: sidebarState === 'timeline', + icon: mdiClockOutline, + onClick: () => openState('timeline'), + }, { tooltip: 'Cloud Space', active: false, @@ -375,15 +412,15 @@ const TopLevelNavigator = () => { }, [ activeBoostHubTeamDomain, generalStatus.boostHubTeams, - showSpaces, storageMap, + showSpaces, + sidebarState, activeStorageId, showSearchModal, - closed, - togglePreferencesModal, toggleShowSearchModal, - sidebarState, toggleShowingCloudIntroModal, + togglePreferencesModal, + openState, ]) return ( diff --git a/src/components/organisms/TrashDetail.tsx b/src/components/organisms/ArchiveDetail.tsx similarity index 88% rename from src/components/organisms/TrashDetail.tsx rename to src/components/organisms/ArchiveDetail.tsx index ae68ff9168..b0507337e6 100644 --- a/src/components/organisms/TrashDetail.tsx +++ b/src/components/organisms/ArchiveDetail.tsx @@ -6,22 +6,24 @@ import { usePreferences } from '../../lib/preferences' import NoteSortingOptionsFragment from '../molecules/NoteSortingOptionsFragment' import { NoteSortingOptions } from '../../lib/sort' import Icon from '../atoms/Icon' -import { mdiTrashCanOutline } from '@mdi/js' -import styled from '../../lib/styled' +import { mdiArchive } from '@mdi/js' +import { values } from '../../lib/db/utils' +import { useTranslation } from 'react-i18next' +import styled from '../../shared/lib/styled' import { flexCenter, borderBottom, selectStyle, -} from '../../lib/styled/styleFunctions' -import { values } from '../../lib/db/utils' +} from '../../shared/lib/styled/styleFunctions' interface TrashDetailProps { storage: NoteStorage } -const TrashDetail = ({ storage }: TrashDetailProps) => { +const ArchiveDetail = ({ storage }: TrashDetailProps) => { const { preferences, setPreferences } = usePreferences() const noteSorting = preferences['general.noteSorting'] + const { t } = useTranslation() const notes = useMemo(() => { return values(storage.noteMap) @@ -70,12 +72,12 @@ const TrashDetail = ({ storage }: TrashDetailProps) => {
    - +
    - Trash + {t('general.archive')}
    -
    +
    {} @@ -148,7 +151,7 @@ const Control = styled.div` display: flex; height: 40px; margin-top: 10px; - ${borderBottom} + ${borderBottom}; .left { flex: 1; } @@ -157,7 +160,7 @@ const Control = styled.div` align-items: center; select { - ${selectStyle} + ${selectStyle}; width: 120px; height: 25px; margin-bottom: 10px; diff --git a/src/components/organisms/IdleNoteDetail.tsx b/src/components/organisms/IdleNoteDetail.tsx deleted file mode 100644 index 4b7165d154..0000000000 --- a/src/components/organisms/IdleNoteDetail.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react' -import Image from '../atoms/Image' -import styled from '../../lib/styled' -import { useTranslation } from 'react-i18next' - -const Container = styled.div` - user-select: none; - position: relative; -` - -const Content = styled.div` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - img { - width: 310px; - max-width: 100%; - padding: 10px 40px; - } - - section { - margin: auto; - display: flex; - width: 70%; - text-align: center; - - div { - text-align: center; - margin: 0 auto; - display: block; - } - } - - h2 { - font-weight: normal; - - span { - margin: 5px auto; - padding: 5px 10px; - width: max-content; - background: #333; - border-radius: 8px; - box-shadow: 0 4px #404040; - } - } - h3 { - margin: 20px auto; - font-weight: normal; - } - h4 { - margin: 0; - font-weight: normal; - } - - @media only screen and (max-width: 970px) { - section { - width: 100%; - display: block; - } - } -` - -const IdleNoteDetail = () => { - const { t } = useTranslation() - return ( - - - -

    {t('note.createkeymessage1')}

    -
    -
    -

    - Ctrl + {t('note.createKey')} -

    -

    {t('note.createKeyWinLin')}

    -
    -

    {t('note.createKeyOr')}

    -
    -

    - + {t('note.createKey')} -

    -

    {t('note.createKeyMac')}

    -
    -
    -
    -
    - ) -} - -export default IdleNoteDetail diff --git a/src/components/organisms/LocalReplace.tsx b/src/components/organisms/LocalReplace.tsx index 5fe6fe6f1b..2d40fbfaa1 100644 --- a/src/components/organisms/LocalReplace.tsx +++ b/src/components/organisms/LocalReplace.tsx @@ -14,7 +14,6 @@ import { mdiMagnify, mdiSubdirectoryArrowLeft, } from '@mdi/js' -import styled from '../../lib/styled/styled' import { LocalSearchInputLeft, SearchResultNavigationDirection, @@ -24,6 +23,7 @@ import LocalSearchButton from '../atoms/search/LocalSearchButton' import { SearchResultItem } from '../atoms/search/SearchResultItem' import { usePreferences } from '../../lib/preferences' import { compareEventKeyWithKeymap } from '../../lib/keymap' +import styled from '../../shared/lib/styled' interface LocalReplaceProps { codeMirror: CodeMirror.EditorFromTextArea @@ -377,7 +377,7 @@ const ReplaceRightContainer = styled.div` align-content: stretch; align-self: stretch; align-items: flex-start; - background-color: ${({ theme }) => theme.searchSecondaryBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.secondary}; ` const ReplaceStyledButton = styled.button` @@ -393,16 +393,16 @@ const ReplaceStyledButton = styled.button` overflow: hidden; transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.navItemColor}; + color: ${({ theme }) => theme.colors.text.secondary}; &:hover { - color: ${({ theme }) => theme.navButtonHoverColor}; + color: ${({ theme }) => theme.colors.text.subtle}; } &:disabled { cursor: default; opacity: 0.5; &:hover { - color: ${({ theme }) => theme.navItemColor}; + color: ${({ theme }) => theme.colors.text.subtle}; } } ` @@ -413,7 +413,7 @@ const LocalReplaceContainer = styled.div` z-index: 5001; width: 100%; - background-color: ${({ theme }) => theme.navBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.tertiary}; ` const LocalReplaceIcon = styled.div` display: flex; diff --git a/src/components/organisms/LocalSearch.tsx b/src/components/organisms/LocalSearch.tsx index a5d52a033c..c670a50f0f 100644 --- a/src/components/organisms/LocalSearch.tsx +++ b/src/components/organisms/LocalSearch.tsx @@ -16,7 +16,6 @@ import { SearchReplaceOptions, } from '../../lib/search/search' import CodeMirror, { MarkerRange, TextMarker } from 'codemirror' -import Icon from '../atoms/Icon' import { mdiArrowDown, mdiArrowUp, @@ -27,11 +26,12 @@ import { mdiSubdirectoryArrowLeft, } from '@mdi/js' import LocalReplace from './LocalReplace' -import styled from '../../lib/styled/styled' import LocalSearchButton from '../atoms/search/LocalSearchButton' import { SearchResultItem } from '../atoms/search/SearchResultItem' import { compareEventKeyWithKeymap } from '../../lib/keymap' import { usePreferences } from '../../lib/preferences' +import styled from '../../shared/lib/styled' +import Icon from '../../shared/components/atoms/Icon' const LOCAL_SEARCH_MAX_RESULTS = 10000 @@ -803,6 +803,7 @@ const SearchOptionsInnerContainer = styled.div` ` const NumResultsContainer = styled.div` + padding-top: 4px; padding-left: 4px; padding-right: 4px; ` @@ -832,7 +833,7 @@ export const LocalSearchInputLeft = styled.div` flex: 1; background-color: transparent; border: none; - color: ${({ theme }) => theme.uiTextColor}; + color: ${({ theme }) => theme.colors.text.primary}; resize: none; max-height: 6em; @@ -850,7 +851,7 @@ export const LocalSearchInputRightContainer = styled.div` align-content: stretch; align-self: stretch; align-items: flex-start; - background-color: ${({ theme }) => theme.searchSecondaryBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.secondary}; ` export const LocalSearchInputRightClose = styled.div` @@ -859,7 +860,7 @@ export const LocalSearchInputRightClose = styled.div` const LocalSearchContainer = styled.div` z-index: 5001; - background-color: ${({ theme }) => theme.navBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.tertiary}; ` function updateMarkerStyle( diff --git a/src/components/organisms/NoteContextView.tsx b/src/components/organisms/NoteContextView.tsx index 01b914ac8a..91b3e4bab3 100644 --- a/src/components/organisms/NoteContextView.tsx +++ b/src/components/organisms/NoteContextView.tsx @@ -1,7 +1,5 @@ import React, { useCallback, useMemo } from 'react' -import styled from '../../lib/styled' import { NoteDoc, NoteStorage } from '../../lib/db/types' -import Icon from '../atoms/Icon' import { mdiClockOutline, mdiTrashCanOutline, @@ -18,7 +16,6 @@ import { import { isTagNameValid } from '../../lib/db/utils' import NoteDetailTagNavigator from '../molecules/NoteDetailTagNavigator' import { useDb } from '../../lib/db' -import { useToast } from '../../lib/toast' import { getFormattedDateTime } from '../../lib/time' import { useTranslation } from 'react-i18next' import { useAnalytics, analyticsEvents } from '../../lib/analytics' @@ -31,6 +28,9 @@ import { import { usePreferences } from '../../lib/preferences' import { usePreviewStyle } from '../../lib/preview' import { useCloudIntroModal } from '../../lib/cloudIntroModal' +import styled from '../../shared/lib/styled' +import Icon from '../../shared/components/atoms/Icon' +import { useToast } from '../../shared/lib/stores/toast' interface NoteContextViewProps { storage: NoteStorage @@ -148,6 +148,7 @@ const NoteContextView = ({ storage, note }: NoteContextViewProps) => { includeFrontMatter ) pushMessage({ + type: 'success', title: 'Markdown export', description: 'Markdown file exported successfully.', }) @@ -169,6 +170,7 @@ const NoteContextView = ({ storage, note }: NoteContextViewProps) => { previewStyle ) pushMessage({ + type: 'success', title: 'HTML export', description: 'HTML file exported successfully.', }) @@ -183,6 +185,12 @@ const NoteContextView = ({ storage, note }: NoteContextViewProps) => { storage.attachmentMap, previewStyle ) + // todo: [komediruzecki-23/05/2021] Pushes message after export is generated but not after user saved the file in dialog! + pushMessage({ + type: 'success', + title: 'PDF export', + description: 'PDF file exported successfully.', + }) }, [note, preferences, pushMessage, storage.attachmentMap, previewStyle]) return ( @@ -316,24 +324,18 @@ const NoteContextView = ({ storage, note }: NoteContextViewProps) => { export default NoteContextView const Container = styled.div` - position: absolute; - top: 0; - right: 0; - bottom: 0; - padding: 8px 0; - display: flex; - flex-direction: column; - width: 350px; - overflow-y: auto; - border-left: solid 1px ${({ theme }) => theme.borderColor}; - flex-shrink: 0; - color: ${({ theme }) => theme.uiTextColor}; + padding: 4px 0; + width: 400px; + // overflow-y: auto; + border-left: 1px solid ${({ theme }) => theme.colors.border.second}; + border-radius: 0; + height: 100vh; ` const Separator = styled.div` height: 1px; margin: 8px 16px; - background-color: ${({ theme }) => theme.borderColor}; + background-color: ${({ theme }) => theme.colors.border.main}; flex-shrink: 0; ` @@ -341,7 +343,7 @@ const ControlItem = styled.div` display: flex; align-items: center; flex-shrink: 0; - color: ${({ theme }) => theme.navItemColor}; + color: ${({ theme }) => theme.colors.text.secondary}; font-size: 14px; ` @@ -361,7 +363,7 @@ interface LabelIconProps { const LabelIcon = ({ path }: LabelIconProps) => { return ( - + ) } @@ -406,8 +408,8 @@ const CloudIntroItemContent = styled.div` ` const TryCloudButton = styled.button` - background-color: ${({ theme }) => theme.primaryColor}; - color: ${({ theme }) => theme.primaryButtonLabelColor}; + background-color: ${({ theme }) => theme.colors.variants.primary.base}; + color: ${({ theme }) => theme.colors.variants.primary.text}; font-size: 12px; border: none; @@ -417,7 +419,7 @@ const TryCloudButton = styled.button` cursor: pointer; } &:focus { - box-shadow: 0 0 0 2px ${({ theme }) => theme.primaryColor}; + box-shadow: 0 0 0 2px ${({ theme }) => theme.colors.background.tertiary}; } &:disabled, &.disabled { @@ -438,19 +440,19 @@ const ButtonItem = styled.button` display: flex; align-items: center; flex-shrink: 0; - color: ${({ theme }) => theme.navItemColor}; + color: ${({ theme }) => theme.colors.text.secondary}; font-size: 14px; - background-color: ${({ theme }) => theme.navItemBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.primary}; &:hover { - background-color: ${({ theme }) => theme.navItemHoverBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.secondary}; } &:active, &.active { - background-color: ${({ theme }) => theme.navItemActiveBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.tertiary}; } &:hover:active, &:hover.active { - background-color: ${({ theme }) => theme.navItemHoverActiveBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.quaternary}; } ` diff --git a/src/components/organisms/NoteDetail.tsx b/src/components/organisms/NoteDetail.tsx index b6b8d228ed..23d0e02ad5 100644 --- a/src/components/organisms/NoteDetail.tsx +++ b/src/components/organisms/NoteDetail.tsx @@ -5,14 +5,8 @@ import { Attachment, NoteStorage, } from '../../lib/db/types' -import styled from '../../lib/styled' import CustomizedCodeEditor from '../atoms/CustomizedCodeEditor' import CustomizedMarkdownPreviewer from '../atoms/CustomizedMarkdownPreviewer' -import { - borderRight, - backgroundColor, - borderTop, -} from '../../lib/styled/styleFunctions' import { ViewModeType } from '../../lib/generalStatus' import { convertItemListToArray, @@ -28,6 +22,12 @@ import { addIpcListener, removeIpcListener } from '../../lib/electronOnly' import { Position } from 'codemirror' import LocalSearch from './LocalSearch' import { SearchReplaceOptions } from '../../lib/search/search' +import { + borderTop, + backgroundColor, + borderRight, +} from '../../shared/lib/styled/styleFunctions' +import styled from '../../shared/lib/styled' type NoteDetailProps = { note: NoteDoc @@ -644,6 +644,8 @@ const Container = styled.div` } z-index: 5001; } + + overflow: hidden; ` const SearchBarContainer = styled.div` diff --git a/src/components/organisms/NoteListNavigator.tsx b/src/components/organisms/NoteListNavigator.tsx deleted file mode 100644 index 9cd32f6481..0000000000 --- a/src/components/organisms/NoteListNavigator.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import React, { useCallback, useRef } from 'react' -import NoteItem from '../molecules/NoteItem' -import styled from '../../lib/styled' -import { borderBottom } from '../../lib/styled/styleFunctions' -import { useTranslation } from 'react-i18next' -import { isWithGeneralCtrlKey } from '../../lib/keyboard' -import { osName } from '../../lib/platform' -import { NoteDoc, PopulatedTagDoc } from '../../lib/db/types' -import { NoteSortingOptions } from '../../lib/sort' -import NoteSortingOptionsFragment from '../molecules/NoteSortingOptionsFragment' -import { usePreferences } from '../../lib/preferences' -import { useRouter } from '../../lib/router' -import { usePathnameWithoutNoteId } from '../../lib/routeParams' -import { MenuItemConstructorOptions } from 'electron' -import { openContextMenu } from '../../lib/electronOnly' - -const Container = styled.div` - display: flex; - flex-direction: column; - overflow: hidden; - height: 100%; - outline: none; -` - -const EmptyItem = styled.li` - user-select: none; - padding: 10px; - color: ${({ theme }) => theme.noteNavEmptyItemColor}; -` - -const NoteListControl = styled.div` - height: 25px; - display: flex; - ${borderBottom}; -` - -const NoteSortingSelect = styled.select` - border: none; - flex: 1; - color: ${({ theme }) => theme.uiTextColor}; - background-color: ${({ theme }) => theme.backgroundColor}; -` - -const NoteList = styled.ul` - flex: 1; - margin: 0; - padding: 0; - list-style: none; - overflow-y: auto; -` - -type NoteListNavigatorProps = { - storageTags: PopulatedTagDoc[] - storageId: string - currentNote?: NoteDoc - currentNoteIndex: number - notes: NoteDoc[] - noteSorting: NoteSortingOptions - setNoteSorting: (noteSorting: NoteSortingOptions) => void - createNote?: () => Promise - basePathname: string - lastCreatedNoteId: string - trashNote: (storageId: string, noteId: string) => Promise - purgeNote: (storageId: string, noteId: string) => void -} - -const NoteListNavigator = ({ - currentNote, - currentNoteIndex, - notes, - noteSorting, - setNoteSorting, - createNote, - storageTags, - storageId, - basePathname, - trashNote, - purgeNote, - lastCreatedNoteId, -}: NoteListNavigatorProps) => { - const { preferences, setPreferences } = usePreferences() - const { t } = useTranslation() - - const currentPathnameWithoutNoteId = usePathnameWithoutNoteId() - const noteListView = preferences['general.noteListView'] - - const applyDefaultNoteListing = useCallback(() => { - setPreferences({ ['general.noteListView']: 'default' }) - }, [setPreferences]) - - const applyCompactListing = useCallback(() => { - setPreferences({ ['general.noteListView']: 'compact' }) - }, [setPreferences]) - - const listRef = useRef(null) - const searchRef = useRef(null) - - const focusList = useCallback(() => { - listRef.current!.focus() - }, []) - - const { push } = useRouter() - const navigateUp = useCallback(() => { - if (currentNoteIndex > 0) { - push(currentPathnameWithoutNoteId + `/${notes[currentNoteIndex - 1]._id}`) - } - }, [notes, currentNoteIndex, push, currentPathnameWithoutNoteId]) - - const navigateDown = useCallback(() => { - if (currentNoteIndex < notes.length - 1) { - push(currentPathnameWithoutNoteId + `/${notes[currentNoteIndex + 1]._id}`) - } - }, [notes, currentNoteIndex, push, currentPathnameWithoutNoteId]) - - const trashOrPurgeCurrentNote = useCallback(() => { - if (currentNote == null) { - return - } - - if (!currentNote.trashed) { - trashNote(storageId, currentNote._id) - } else { - purgeNote(storageId, currentNote._id) - } - focusList() - }, [trashNote, purgeNote, currentNote, storageId, focusList]) - - const handleListKeyDown: React.KeyboardEventHandler = useCallback( - (event) => { - switch (event.key) { - case 'Delete': - if (osName !== 'macos') { - trashOrPurgeCurrentNote() - } - break - case 'Backspace': - if (isWithGeneralCtrlKey(event)) { - trashOrPurgeCurrentNote() - } - break - case 's': - searchRef.current!.focus() - break - case 'j': - navigateDown() - break - case 'k': - navigateUp() - break - } - }, - [trashOrPurgeCurrentNote, navigateDown, navigateUp] - ) - - const openListContextMenu: React.MouseEventHandler = useCallback( - (event) => { - event.preventDefault() - - const menuItems: MenuItemConstructorOptions[] = [ - { - type: 'normal', - label: 'Default View', - click: applyDefaultNoteListing, - }, - { - type: 'normal', - label: 'Compact View', - click: applyCompactListing, - }, - ] - - if (createNote != null) { - menuItems.unshift( - { - type: 'normal', - label: 'New Note', - click: createNote, - }, - { - type: 'separator', - } - ) - } - - openContextMenu({ menuItems }) - }, - [createNote, applyDefaultNoteListing, applyCompactListing] - ) - - return ( - - - ) => { - setNoteSorting(event.target.value as NoteSortingOptions) - }} - > - - - - - {notes.map((note) => { - const noteIsCurrentNote = note._id === currentNote?._id - const noteTags = note.tags.reduce( - (result: PopulatedTagDoc[], tagName) => { - const tagElement = storageTags.find( - (storageTag) => storageTag.name == tagName - ) - if (tagElement !== undefined) { - result.push(tagElement) - } - return result - }, - [] - ) - return ( -
  • - -
  • - ) - })} - {notes.length === 0 && {t('note.nothing')}} -
    -
    - ) -} - -export default NoteListNavigator diff --git a/src/components/organisms/NotePageToolbar.tsx b/src/components/organisms/NotePageToolbar.tsx deleted file mode 100644 index 5ab9883969..0000000000 --- a/src/components/organisms/NotePageToolbar.tsx +++ /dev/null @@ -1,406 +0,0 @@ -import React, { useCallback, MouseEventHandler, useEffect } from 'react' -import styled from '../../lib/styled' -import { NoteDoc, NoteStorage } from '../../lib/db/types' -import { - mdiViewSplitVertical, - mdiStarOutline, - mdiStar, - mdiEye, - mdiPencil, - mdiChevronRight, - mdiChevronLeft, -} from '@mdi/js' -import { borderBottom, flexCenter } from '../../lib/styled/styleFunctions' -import ToolbarIconButton from '../atoms/ToolbarIconButton' -import { useGeneralStatus } from '../../lib/generalStatus' -import NotePageToolbarNoteHeader from '../molecules/NotePageToolbarNoteHeader' -import { - exportNoteAsHtmlFile, - exportNoteAsMarkdownFile, - convertNoteDocToPdfBuffer, -} from '../../lib/exports' -import { usePreferences } from '../../lib/preferences' -import { usePreviewStyle } from '../../lib/preview' -import { useTranslation } from 'react-i18next' -import { useDb } from '../../lib/db' -import { useRouteParams } from '../../lib/routeParams' -import { useToast } from '../../lib/toast' -import { - openContextMenu, - showSaveDialog, - getPathByName, - addIpcListener, - removeIpcListener, - writeFile, -} from '../../lib/electronOnly' -import NotePageToolbarFolderHeader from '../molecules/NotePageToolbarFolderHeader' -import path from 'path' -import pathParse from 'path-parse' -import { filenamify } from '../../lib/string' - -const Container = styled.div` - display: flex; - overflow: hidden; - height: 44px; - flex-shrink: 0; - padding: 0 8px; - ${borderBottom}; - align-items: center; - & > .left { - flex: 1; - display: flex; - align-items: center; - overflow: hidden; - } -` - -const Control = styled.div` - ${flexCenter} -` - -interface NotePageToolbarProps { - storage: NoteStorage - note?: NoteDoc -} - -const NotePageToolbar = ({ storage, note }: NotePageToolbarProps) => { - const { t } = useTranslation() - const { bookmarkNote, unbookmarkNote } = useDb() - const { setPreferences, preferences } = usePreferences() - - const editorControlMode = preferences['editor.controlMode'] - - const { previewStyle } = usePreviewStyle() - const { generalStatus, setGeneralStatus } = useGeneralStatus() - const { noteViewMode, preferredEditingViewMode } = generalStatus - const { pushMessage } = useToast() - - const storageId = storage.id - const storageName = storage.name - - const noteId = note?._id - - const bookmark = useCallback(async () => { - if (noteId == null) { - return - } - await bookmarkNote(storageId, noteId) - }, [storageId, noteId, bookmarkNote]) - - const unbookmark = useCallback(async () => { - if (noteId == null) { - return - } - await unbookmarkNote(storageId, noteId) - }, [storageId, noteId, unbookmarkNote]) - - const selectEditMode = useCallback(() => { - setGeneralStatus({ - noteViewMode: 'edit', - preferredEditingViewMode: 'edit', - }) - }, [setGeneralStatus]) - - const selectSplitMode = useCallback(() => { - setGeneralStatus({ - noteViewMode: 'split', - preferredEditingViewMode: 'split', - }) - }, [setGeneralStatus]) - - const selectPreviewMode = useCallback(() => { - setGeneralStatus({ - noteViewMode: 'preview', - }) - }, [setGeneralStatus]) - - const togglePreviewMode = useCallback(() => { - if (noteViewMode === 'preview') { - if (preferredEditingViewMode === 'edit') { - selectEditMode() - } else { - selectSplitMode() - } - } else { - selectPreviewMode() - } - }, [ - noteViewMode, - preferredEditingViewMode, - selectEditMode, - selectSplitMode, - selectPreviewMode, - ]) - - useEffect(() => { - addIpcListener('toggle-preview-mode', togglePreviewMode) - return () => { - removeIpcListener('toggle-preview-mode', togglePreviewMode) - } - }, [togglePreviewMode]) - - const toggleSplitEditMode = useCallback(() => { - if (noteViewMode === 'edit') { - selectSplitMode() - } else { - selectEditMode() - } - }, [noteViewMode, selectSplitMode, selectEditMode]) - - useEffect(() => { - addIpcListener('toggle-split-edit-mode', toggleSplitEditMode) - return () => { - removeIpcListener('toggle-split-edit-mode', toggleSplitEditMode) - } - }, [toggleSplitEditMode]) - - const includeFrontMatter = preferences['markdown.includeFrontMatter'] - - useEffect(() => { - const handler = () => { - if (note == null) { - return - } - showSaveDialog({ - properties: ['createDirectory', 'showOverwriteConfirmation'], - buttonLabel: 'Save', - defaultPath: path.join( - getPathByName('home'), - filenamify(note.title) + '.md' - ), - filters: [ - { - name: 'Markdown', - extensions: ['md'], - }, - { - name: 'HTML', - extensions: ['html'], - }, - { - name: 'PDF', - extensions: ['pdf'], - }, - ], - }).then(async (result) => { - if (result.canceled || result.filePath == null) { - return - } - const parsedFilePath = pathParse(result.filePath) - switch (parsedFilePath.ext) { - case '.html': - await exportNoteAsHtmlFile( - parsedFilePath.dir, - parsedFilePath.name, - note, - preferences['markdown.codeBlockTheme'], - preferences['general.theme'], - pushMessage, - storage.attachmentMap, - previewStyle - ) - pushMessage({ - title: 'HTML export', - description: 'HTML file exported successfully.', - }) - return - case '.pdf': - try { - const pdfBuffer = await convertNoteDocToPdfBuffer( - note, - preferences['markdown.codeBlockTheme'], - preferences['general.theme'], - pushMessage, - storage.attachmentMap, - previewStyle - ) - await writeFile(result.filePath, pdfBuffer) - } catch (error) { - console.error(error) - pushMessage({ - title: 'PDF export failed', - description: error.message, - }) - } - return - case '.md': - default: - await exportNoteAsMarkdownFile( - parsedFilePath.dir, - parsedFilePath.name, - note, - storage.attachmentMap, - includeFrontMatter - ) - pushMessage({ - title: 'Markdown export', - description: 'Markdown file exported successfully.', - }) - return - } - }) - } - addIpcListener('save-as', handler) - return () => { - removeIpcListener('save-as', handler) - } - }, [ - note, - includeFrontMatter, - preferences, - previewStyle, - pushMessage, - storage.attachmentMap, - ]) - - const routeParams = useRouteParams() - const folderPathname = - note == null - ? routeParams.name === 'storages.notes' - ? routeParams.folderPathname - : '/' - : note.folderPathname - - const openTopbarSwitchSelectorContextMenu: MouseEventHandler = useCallback( - (event) => { - event.preventDefault() - openContextMenu({ - menuItems: [ - { - type: 'normal', - label: 'Use 2 toggles layout', - click: () => { - setPreferences({ - 'editor.controlMode': '2-toggles', - }) - }, - }, - { - type: 'normal', - label: 'Use 3 buttons layout', - click: () => { - setPreferences({ - 'editor.controlMode': '3-buttons', - }) - }, - }, - ], - }) - }, - [setPreferences] - ) - - const toggleBookmark = useCallback(() => { - if (note == null) { - return - } - if (note.data.bookmarked) { - unbookmark() - } else { - bookmark() - } - }, [note, unbookmark, bookmark]) - - useEffect(() => { - addIpcListener('toggle-bookmark', toggleBookmark) - return () => { - removeIpcListener('toggle-bookmark', toggleBookmark) - } - }) - - const toggleContextView = useCallback(() => { - setGeneralStatus((previousGeneralStatus) => { - return { - showingNoteContextMenu: !previousGeneralStatus.showingNoteContextMenu, - } - }) - }, [setGeneralStatus]) - - return ( - -
    - {note == null ? ( - - ) : ( - - )} -
    - - {note != null && ( - - {editorControlMode === '3-buttons' ? ( - <> - - - - - ) : ( - <> - {noteViewMode !== 'preview' && ( - - )} - {noteViewMode !== 'preview' ? ( - - ) : ( - - )} - - )} - - - - )} -
    - ) -} - -export default NotePageToolbar diff --git a/src/components/organisms/NoteStorageNavigator.tsx b/src/components/organisms/NoteStorageNavigator.tsx deleted file mode 100644 index d707c50f94..0000000000 --- a/src/components/organisms/NoteStorageNavigator.tsx +++ /dev/null @@ -1,348 +0,0 @@ -import React, { useCallback, useEffect, useMemo } from 'react' -import { useRouter } from '../../lib/router' -import { useDb } from '../../lib/db' -import styled from '../../lib/styled' -import { useDialog, DialogIconTypes } from '../../lib/dialog' -import { usePreferences } from '../../lib/preferences' -import StorageNavigatorFragment from '../molecules/StorageNavigatorFragment' -import BookmarkNavigatorFragment from '../molecules/BookmarkNavigatorFragment' -import { NoteStorage } from '../../lib/db/types' -import { - openContextMenu, - addIpcListener, - removeIpcListener, -} from '../../lib/electronOnly' -import { values, getFolderNameFromPathname } from '../../lib/db/utils' -import { MenuItemConstructorOptions } from 'electron' -import { useStorageRouter } from '../../lib/storageRouter' -import { useRouteParams } from '../../lib/routeParams' -import { mdiPlus, mdiFolderOutline, mdiTag } from '@mdi/js' -import Icon from '../atoms/Icon' -import { flexCenter, textOverflow } from '../../lib/styled/styleFunctions' -import { noteDetailFocusTitleInputEventEmitter } from '../../lib/events' -import NavigatorSeparator from '../atoms/NavigatorSeparator' -import { useTranslation } from 'react-i18next' -import { useSearchModal } from '../../lib/searchModal' - -interface NoteStorageNavigatorProps { - storage: NoteStorage -} - -const NoteStorageNavigator = ({ storage }: NoteStorageNavigatorProps) => { - const { - createStorage, - storageMap, - createNote, - renameStorage, - removeStorage, - } = useDb() - const { prompt, messageBox } = useDialog() - const { push, hash } = useRouter() - const { navigate } = useStorageRouter() - const { togglePreferencesModal } = usePreferences() - const routeParams = useRouteParams() - const storageId = storage.id - const { t } = useTranslation() - - const openCreateStorageDialog = useCallback(() => { - prompt({ - title: 'Create a Space', - message: 'Enter name of a space to create', - iconType: DialogIconTypes.Question, - submitButtonLabel: 'Create Space', - onClose: async (value: string | null) => { - if (value == null) return - const storage = await createStorage(value) - push(`/app/storages/${storage.id}/notes`) - }, - }) - }, [prompt, createStorage, push]) - - const openStorageContextMenu = useCallback( - (event: React.MouseEvent) => { - event.preventDefault() - event.stopPropagation() - - const storages = values(storageMap) - openContextMenu({ - menuItems: [ - { - type: 'normal', - label: t('storage.rename'), - click: async () => { - prompt({ - title: `Rename "${storage.name}" Space`, - message: t('storage.renameMessage'), - iconType: DialogIconTypes.Question, - defaultValue: storage.name, - submitButtonLabel: t('storage.rename'), - onClose: async (value: string | null) => { - if (value == null) return - renameStorage(storage.id, value) - }, - }) - }, - }, - { - type: 'normal', - label: t('storage.remove'), - click: async () => { - messageBox({ - title: `Remove "${storage.name}" Space`, - message: - storage.type === 'fs' - ? "This operation won't delete the actual space folder. You can add it to the app again." - : t('storage.removeMessage'), - iconType: DialogIconTypes.Warning, - buttons: [t('storage.remove'), t('general.cancel')], - defaultButtonIndex: 0, - cancelButtonIndex: 1, - onClose: (value: number | null) => { - if (value === 0) { - removeStorage(storage.id) - } - }, - }) - }, - }, - { - type: 'separator', - }, - { - type: 'normal', - label: 'Preferences', - click: () => { - togglePreferencesModal() - }, - }, - { - type: 'separator', - }, - ...storages - .filter((storage) => { - return storage.id !== storageId - }) - .map((storage) => { - return { - type: 'normal', - label: `Switch to ${storage.name} storage`, - click: () => { - navigate(storage.id) - }, - } - }), - { - type: 'separator', - }, - { - type: 'normal', - label: 'New Space', - click: () => { - openCreateStorageDialog() - }, - }, - ], - }) - }, - [ - storageMap, - t, - prompt, - storage.name, - storage.id, - storage.type, - renameStorage, - messageBox, - togglePreferencesModal, - storageId, - navigate, - openCreateStorageDialog, - removeStorage, - ] - ) - - const extraNewNoteLabel = useMemo(() => { - switch (routeParams.name) { - case 'storages.notes': - if (routeParams.folderPathname !== '/') { - return ( - <> - in {' '} - {getFolderNameFromPathname(routeParams.folderPathname)} - - ) - } - break - case 'storages.tags.show': - return ( - <> - with - {routeParams.tagName} - - ) - } - return null - }, [routeParams]) - - const createNoteByRoute = useCallback(async () => { - let folderPathname = '/' - let tags: string[] = [] - let baseHrefAfterCreate = `/app/storages/${storageId}/notes` - switch (routeParams.name) { - case 'storages.tags.show': - tags = [routeParams.tagName] - baseHrefAfterCreate = `/app/storages/${storageId}/tags/${routeParams.tagName}` - break - case 'storages.notes': - if (routeParams.folderPathname !== '/') { - folderPathname = routeParams.folderPathname - baseHrefAfterCreate = `/app/storages/${storageId}/notes${folderPathname}` - } - break - } - - const note = await createNote(storageId, { - folderPathname, - tags, - }) - if (note == null) { - return - } - - push(`${baseHrefAfterCreate}/${note._id}#new`) - }, [storageId, routeParams, push, createNote]) - - useEffect(() => { - if (hash === '#new') { - push({ hash: '' }) - setImmediate(() => { - noteDetailFocusTitleInputEventEmitter.dispatch() - }) - } - }, [push, hash]) - - useEffect(() => { - const handler = () => { - createNoteByRoute() - } - addIpcListener('new-note', handler) - return () => { - removeIpcListener('new-note', handler) - } - }, [createNoteByRoute]) - - const { toggleShowSearchModal } = useSearchModal() - - useEffect(() => { - const handler = () => { - toggleShowSearchModal() - } - addIpcListener('search', handler) - return () => { - removeIpcListener('search', handler) - } - }, [toggleShowSearchModal]) - - return ( - - -
    {storage.name}
    -
    - - -
    - -
    -
    New Note
    - {extraNewNoteLabel != null && ( -
    {extraNewNoteLabel}
    - )} -
    - - - - - - -
    - ) -} - -export default NoteStorageNavigator - -const NavigatorContainer = styled.nav` - display: flex; - flex-direction: column; - height: 100%; - background-color: ${({ theme }) => theme.navBackgroundColor}; -` - -const ScrollableContainer = styled.div` - flex: 1; - padding: 8px; - overflow: auto; -` - -const TopButton = styled.button` - height: 50px; - display: flex; - flex-direction: row; - align-items: center; - cursor: pointer; - text-align: left; - padding: 0 16px; - border: none; - color: ${({ theme }) => theme.navLabelColor}; - background-color: transparent; - background-color: ${({ theme }) => theme.navItemBackgroundColor}; - margin: 4px 0; - & > .topButtonLabel { - font-size: 14px; - padding-right: 4px; - ${textOverflow} - } -` - -const NewNoteButton = styled.button` - margin: 4px 8px; - height: 28px; - color: ${({ theme }) => theme.primaryButtonLabelColor}; - background-color: ${({ theme }) => theme.primaryButtonBackgroundColor}; - border: none; - border-radius: 3px; - cursor: pointer; - text-align: left; - align-items: center; - display: flex; - padding: 0 8px 0 4px; - font-size: 14px; - &:hover { - background-color: ${({ theme }) => theme.primaryButtonHoverBackgroundColor}; - .extra { - display: flex; - } - } - - & > .icon { - width: 28px; - height: 28px; - ${flexCenter}; - flex-shrink: 0; - font-size: 20px; - } - & > .label { - white-space: nowrap; - flex-shrink: 0; - } - & > .extra { - display: none; - font-size: 12px; - margin-left: 5px; - ${textOverflow}; - align-items: center; - & > .icon { - flex-shrink: 0; - margin: 0 4px; - } - } -` diff --git a/src/components/organisms/SearchModal.tsx b/src/components/organisms/SearchModal.tsx index 6a7e929936..fb3ee82c6a 100644 --- a/src/components/organisms/SearchModal.tsx +++ b/src/components/organisms/SearchModal.tsx @@ -5,36 +5,36 @@ import React, { useRef, KeyboardEvent, } from 'react' -import styled from '../../lib/styled' import { NoteDoc, NoteStorage } from '../../lib/db/types' import { useEffectOnce, useDebounce } from 'react-use' -import { excludeNoteIdPrefix, values } from '../../lib/db/utils' -import { escapeRegExp } from '../../lib/string' +import { excludeNoteIdPrefix } from '../../lib/db/utils' import { useSearchModal } from '../../lib/searchModal' -import { - border, - borderBottom, - borderTop, - flexCenter, - textOverflow, -} from '../../lib/styled/styleFunctions' +import { flexCenter, textOverflow } from '../../lib/styled/styleFunctions' import { mdiMagnify, mdiClose, mdiCardTextOutline } from '@mdi/js' -import Icon from '../atoms/Icon' import SearchModalNoteResultItem from '../molecules/SearchModalNoteResultItem' import { useStorageRouter } from '../../lib/storageRouter' import { - getMatchData, getSearchResultKey, NoteSearchData, SearchResult, SEARCH_DEBOUNCE_TIMEOUT, GLOBAL_MERGE_SAME_LINE_RESULTS_INTO_ONE, - TagSearchResult, } from '../../lib/search/search' import CustomizedCodeEditor from '../atoms/CustomizedCodeEditor' import CodeMirror from 'codemirror' -import { BaseTheme } from '../../lib/styled/BaseTheme' import cc from 'classcat' +import styled from '../../shared/lib/styled' +import { BaseTheme } from '../../shared/lib/styled/types' +import { + border, + borderBottom, + borderTop, +} from '../../shared/lib/styled/styleFunctions' +import { + getSearchResultItems, + getSearchRegex, +} from '../../lib/v2/mappers/local/searchResults' +import Icon from '../../shared/components/atoms/Icon' interface SearchModalProps { storage: NoteStorage @@ -69,10 +69,6 @@ const SearchModal = ({ storage }: SearchModalProps) => { focusTextAreaInput() }) - const getSearchRegex = useCallback((rawSearch) => { - return new RegExp(escapeRegExp(rawSearch), 'gim') - }, []) - const { navigateToNoteWithEditorFocus: _navFocusEditor } = useStorageRouter() const navFocusEditor = useCallback( @@ -90,48 +86,13 @@ const SearchModal = ({ storage }: SearchModalProps) => { setSearching(false) return } - const notes = values(storage.noteMap) - const regex = getSearchRegex(searchValue) - // todo: [komediruzecki-01/12/2020] Here we could have buttons (toggles) for content/title/tag search! (by tag color?) - // for now, it's only content search - const searchResultData: NoteSearchData[] = [] - notes.forEach((note) => { - if (note.trashed) { - return - } - const matchDataContent = getMatchData(note.content, regex) - - const titleMatchResult = note.title.match(regex) - - const titleSearchResult = - titleMatchResult != null ? titleMatchResult[0] : null - const tagSearchResults = note.tags.reduce( - (searchResults, tagName) => { - const matchResult = tagName.match(regex) - if (matchResult != null) { - searchResults.push({ - tagName, - matchString: matchResult[0], - }) - } - return searchResults - }, - [] - ) - - if ( - titleSearchResult || - tagSearchResults.length > 0 || - matchDataContent.length > 0 - ) { - const noteResultKey = excludeNoteIdPrefix(note._id) - noteToSearchResultMap[noteResultKey] = matchDataContent - searchResultData.push({ - titleSearchResult, - tagSearchResults, - note: note, - results: matchDataContent, - }) + const searchResultData = getSearchResultItems(storage, searchValue) + searchResultData.forEach((searchResult) => { + if (searchResult.item.type === 'noteContent') { + const noteResultKey = excludeNoteIdPrefix( + searchResult.item.result._id + ) + noteToSearchResultMap[noteResultKey] = searchResult.results } }) @@ -206,7 +167,7 @@ const SearchModal = ({ storage }: SearchModalProps) => { } } }, - [getSearchRegex] + [] ) const focusEditorOnSelectedItem = useCallback( @@ -301,19 +262,23 @@ const SearchModal = ({ storage }: SearchModalProps) => {
    No Results
    )} {!searching && - resultList.map((result) => { + resultList.map((searchData) => { + if (searchData.item.type === 'folder') { + return + } + const noteResult: NoteDoc = searchData.item.result return ( ` & > .container { position: relative; margin: 50px auto 0; - background-color: ${({ theme }) => theme.navBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.primary}; width: calc(100% - 15px); max-width: 720px; overflow: hidden; @@ -399,7 +364,7 @@ const Container = styled.div` flex: 1; background-color: transparent; border: none; - color: ${({ theme }) => theme.uiTextColor}; + color: ${({ theme }) => theme.colors.text.primary}; resize: none; max-height: 4em; @@ -413,12 +378,12 @@ const Container = styled.div` flex: 1; & > .searching { text-align: center; - color: ${({ theme }) => theme.disabledUiTextColor}; + color: ${({ theme }) => theme.colors.text.disabled}; padding: 10px; } & > .empty { text-align: center; - color: ${({ theme }) => theme.disabledUiTextColor}; + color: ${({ theme }) => theme.colors.text.disabled}; padding: 10px; } & > .item { @@ -439,8 +404,8 @@ const Container = styled.div` const EditorPreview = styled.div` .marked { background-color: ${({ theme }) => - theme.searchHighlightSubtleBackgroundColor}; - color: ${({ theme }) => theme.searchHighlightTextColor} !important; + theme.codeEditorMarkedTextBackgroundColor}; + color: #212121 !important; padding: 3px; } @@ -450,10 +415,11 @@ const EditorPreview = styled.div` } .selected { - background-color: ${({ theme }) => theme.searchHighlightBackgroundColor}; + background-color: ${({ theme }) => + theme.codeEditorSelectedTextBackgroundColor}; } - background-color: ${({ theme }) => theme.navBackgroundColor}; + background-color: ${({ theme }) => theme.colors.background.primary}; ${borderTop}; width: 100%; @@ -477,7 +443,7 @@ const EditorPreview = styled.div` flex: 1; ${textOverflow} &.empty { - color: ${({ theme }) => theme.disabledUiTextColor}; + color: ${({ theme }) => theme.colors.text.disabled}; } } & > .icon { @@ -502,14 +468,14 @@ const EditorPreview = styled.div` cursor: pointer; transition: color 200ms ease-in-out; - color: ${({ theme }) => theme.navItemColor}; + color: ${({ theme }) => theme.colors.text.secondary}; &:hover { - color: ${({ theme }) => theme.navButtonHoverColor}; + color: ${({ theme }) => theme.colors.text.subtle}; } &:active, &.active { - color: ${({ theme }) => theme.navButtonActiveColor}; + color: ${({ theme }) => theme.colors.text.link}; } } } diff --git a/src/components/organisms/SidebarContainer.tsx b/src/components/organisms/SidebarContainer.tsx new file mode 100644 index 0000000000..9ff3ae1138 --- /dev/null +++ b/src/components/organisms/SidebarContainer.tsx @@ -0,0 +1,842 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useRouter } from '../../lib/router' +import { useDb } from '../../lib/db' +import { usePreferences } from '../../lib/preferences' +import { NoteStorage } from '../../lib/db/types' +import { + openContextMenu, + addIpcListener, + removeIpcListener, +} from '../../lib/electronOnly' +import { entries, getTimelineHref, values } from '../../lib/db/utils' +import { MenuItemConstructorOptions } from 'electron' +import { useStorageRouter } from '../../lib/storageRouter' +import { useRouteParams } from '../../lib/routeParams' +import { + mdiFolderOutline, + mdiLogin, + mdiLogout, + mdiMenu, + mdiMessageQuestion, + mdiPlus, + mdiTextBoxPlusOutline, +} from '@mdi/js' +import { noteDetailFocusTitleInputEventEmitter } from '../../lib/events' +import { useTranslation } from 'react-i18next' +import { useSearchModal } from '../../lib/searchModal' +import styled from '../../shared/lib/styled' +import Button from '../../shared/components/atoms/Button' +import Sidebar from '../../shared/components/organisms/Sidebar' +import cc from 'classcat' +import { + SidebarState, + SidebarTreeSortingOrders, +} from '../../shared/lib/sidebar' +import { MenuTypes, useContextMenu } from '../../shared/lib/stores/contextMenu' +import { SidebarToolbarRow } from '../../shared/components/organisms/Sidebar/molecules/SidebarToolbar' +import { mapToolbarRows } from '../../lib/v2/mappers/local/sidebarRows' +import { useGeneralStatus } from '../../lib/generalStatus' +import { mapHistory } from '../../lib/v2/mappers/local/sidebarHistory' +import { SidebarSearchResult } from '../../shared/components/organisms/Sidebar/molecules/SidebarSearch' +import { AppUser } from '../../shared/lib/mappers/users' +import useApi from '../../shared/lib/hooks/useApi' +import { useDebounce } from 'react-use' +import { + GetSearchResultsRequestQuery, + NoteSearchData, +} from '../../lib/search/search' +import { + getSearchResultItems, + mapSearchResults, +} from '../../lib/v2/mappers/local/searchResults' +import { useLocalUI } from '../../lib/v2/hooks/local/useLocalUI' +import { mapTree } from '../../lib/v2/mappers/local/sidebarTree' +import { useLocalDB } from '../../lib/v2/hooks/local/useLocalDB' +import { useLocalDnd } from '../../lib/v2/hooks/local/useLocalDnd' +import { CollapsableType } from '../../lib/v2/stores/sidebarCollapse' +import { useSidebarCollapse } from '../../lib/v2/stores/sidebarCollapse' +import { useCloudIntroModal } from '../../lib/cloudIntroModal' +import { mapLocalSpace } from '../../lib/v2/mappers/local/sidebarSpaces' +import { osName } from '../../shared/lib/platform' +import { mapTimelineItems } from '../../lib/v2/mappers/local/timelineRows' +import { + SidebarSpace, + SidebarSpaceContentRow, +} from '../../shared/components/organisms/Sidebar/molecules/SidebarSpaces' +import { useBoostHub } from '../../lib/boosthub' +import { DialogIconTypes, useDialog } from '../../shared/lib/stores/dialog' +import BasicInputFormLocal from '../v2/organisms/BasicInputFormLocal' +import { useToast } from '../../shared/lib/stores/toast' +import { useModal } from '../../shared/lib/stores/modal' + +interface SidebarContainerProps { + hideSidebar?: boolean + initialSidebarState?: SidebarState + storage?: NoteStorage +} + +const SidebarContainer = ({ + initialSidebarState, + storage, + hideSidebar, +}: SidebarContainerProps) => { + const { + createNote, + createStorage, + storageMap, + renameStorage, + removeStorage, + } = useDb() + const { pushMessage } = useToast() + const { openModal, closeLastModal } = useModal() + const { messageBox } = useDialog() + const { push, hash, pathname } = useRouter() + const { navigate } = useStorageRouter() + const { preferences, openTab, togglePreferencesModal } = usePreferences() + const routeParams = useRouteParams() + const { t } = useTranslation() + const boostHubUserInfo = preferences['cloud.user'] + const { signOut } = useBoostHub() + + // todo: [komediruzecki-22/05/2021] add this to local UI as well + const openCreateStorageDialog = useCallback(() => { + openModal( + { + if (workspaceName == '') { + pushMessage({ + title: 'Cannot rename workspace', + description: 'Workspace name should not be empty.', + }) + closeLastModal() + return + } + const storage = await createStorage(workspaceName) + push(`/app/storages/${storage.id}/notes`) + closeLastModal() + }} + />, + { + showCloseIcon: true, + title: 'Create a space', + } + ) + }, [closeLastModal, createStorage, openModal, push, pushMessage]) + + const openStorageContextMenu = useCallback( + (event: React.MouseEvent) => { + if (storage == null) { + return + } + event.preventDefault() + event.stopPropagation() + + const storages = values(storageMap) + const workspaceId = storage.id + openContextMenu({ + menuItems: [ + { + type: 'normal', + label: t('storage.rename'), + click: async () => { + openModal( + { + if (workspaceName == '') { + pushMessage({ + title: 'Cannot rename workspace', + description: 'Workspace name should not be empty.', + }) + closeLastModal() + return + } + renameStorage(storage.id, workspaceName) + closeLastModal() + }} + />, + { + showCloseIcon: true, + title: `Rename "${storage.name}" Space`, + } + ) + }, + }, + { + type: 'normal', + label: t('storage.remove'), + click: async () => { + messageBox({ + title: `Remove "${storage.name}" Space`, + message: + storage.type === 'fs' + ? "This operation won't delete the actual space folder. You can add it to the app again." + : t('storage.removeMessage'), + iconType: DialogIconTypes.Warning, + buttons: [ + { + label: t('storage.remove'), + onClick: () => { + removeStorage(storage.id) + }, + }, + { label: t('general.cancel') }, + ], + }) + }, + }, + { + type: 'separator', + }, + { + type: 'normal', + label: 'Preferences', + click: () => { + togglePreferencesModal() + }, + }, + { + type: 'separator', + }, + ...storages + .filter((storage) => { + return storage.id !== workspaceId + }) + .map((storage) => { + return { + type: 'normal', + label: `Switch to ${storage.name} storage`, + click: () => { + navigate(storage.id) + }, + } + }), + { + type: 'separator', + }, + { + type: 'normal', + label: 'New Space', + click: () => { + openCreateStorageDialog() + }, + }, + ], + }) + }, + [ + storage, + storageMap, + t, + openModal, + renameStorage, + closeLastModal, + pushMessage, + messageBox, + removeStorage, + togglePreferencesModal, + navigate, + openCreateStorageDialog, + ] + ) + + // const extraNewNoteLabel = useMemo(() => { + // switch (routeParams.name) { + // case 'storages.notes': + // if (routeParams.folderPathname !== '/') { + // return ( + // <> + // in {' '} + // {getFolderNameFromPathname(routeParams.folderPathname)} + // + // ) + // } + // break + // case 'storages.tags.show': + // return ( + // <> + // with + // {routeParams.tagName} + // + // ) + // } + // return null + // }, [routeParams]) + + const createNoteByRoute = useCallback(async () => { + if (storage == null) { + return + } + const workspaceId = storage.id + let folderPathname = '/' + let tags: string[] = [] + let baseHrefAfterCreate = `/app/storages/${workspaceId}/notes` + switch (routeParams.name) { + case 'workspaces.labels.show': + tags = [routeParams.tagName] + baseHrefAfterCreate = `/app/storages/${workspaceId}/tags/${routeParams.tagName}` + break + case 'workspaces.notes': + if (routeParams.folderPathname !== '/') { + folderPathname = routeParams.folderPathname + baseHrefAfterCreate = `/app/storages/${workspaceId}/notes${folderPathname}` + } + break + } + + const note = await createNote(workspaceId, { + folderPathname, + tags, + }) + if (note == null) { + return + } + + push(`${baseHrefAfterCreate}/${note._id}#new`) + }, [storage, routeParams, push, createNote]) + + useEffect(() => { + if (hash === '#new') { + push({ hash: '' }) + setImmediate(() => { + noteDetailFocusTitleInputEventEmitter.dispatch() + }) + } + }, [push, hash]) + + useEffect(() => { + const handler = () => { + createNoteByRoute() + } + addIpcListener('new-note', handler) + return () => { + removeIpcListener('new-note', handler) + } + }, [createNoteByRoute]) + + const { toggleShowSearchModal } = useSearchModal() + + useEffect(() => { + const handler = () => { + toggleShowSearchModal() + } + addIpcListener('search', handler) + return () => { + removeIpcListener('search', handler) + } + }, [toggleShowSearchModal]) + + // Sidebar related items - properly implement after mapping + const { generalStatus, setGeneralStatus } = useGeneralStatus() + const { popup } = useContextMenu() + const [showSpaces, setShowSpaces] = useState(false) + const [sidebarState, setSidebarState] = useState( + hideSidebar != null && hideSidebar + ? undefined + : initialSidebarState != null + ? initialSidebarState + : generalStatus.lastSidebarState + ) + const [sidebarSearchQuery, setSidebarSearchQuery] = useState('') + + useEffect(() => { + setGeneralStatus({ lastSidebarState: sidebarState }) + }, [sidebarState, setSidebarState, setGeneralStatus]) + + const openState = useCallback((state: SidebarState) => { + setSidebarState((prev) => (prev === state ? undefined : state)) + }, []) + const { toggleShowingCloudIntroModal } = useCloudIntroModal() + + const toolbarRows: SidebarToolbarRow[] = useMemo(() => { + if (storage != null) { + return mapToolbarRows( + showSpaces, + setShowSpaces, + openState, + openTab, + toggleShowingCloudIntroModal, + sidebarState, + storage + ) + } else { + return [ + { + tooltip: 'Spaces', + active: showSpaces, + icon: mdiMenu, + onClick: () => setShowSpaces((prev) => !prev), + }, + ] as SidebarToolbarRow[] + } + }, [ + openState, + openTab, + showSpaces, + sidebarState, + storage, + toggleShowingCloudIntroModal, + ]) + + const localSpaces = values(storageMap) + const sidebarResize = useCallback( + (width: number) => setGeneralStatus({ sideBarWidth: width }), + [setGeneralStatus] + ) + const setSearchQuery = useCallback((val: string) => { + setSidebarSearchQuery(val) + }, []) + + const historyItems = useMemo(() => { + if (storage == null) { + return [] + } + return mapHistory( + // implement history items for search + [], + push, + storage.noteMap, + storage.folderMap, + storage + ) + }, [push, storage]) + const { submit: submitSearch, sending: fetchingSearchResults } = useApi< + { query: any }, + { results: NoteSearchData[] } + >({ + api: ({ query }: { query: any }) => { + // return new Promise(() => { + return Promise.resolve({ + results: getSearchResultItems(storage, query.query), + }) + // }) + }, + cb: ({ results }) => { + // console.log('got results', results) + setSearchResults(mapSearchResults(results, push, storage)) + }, + }) + + const [isNotDebouncing, cancel] = useDebounce( + async () => { + if (storage == null || sidebarSearchQuery.trim() === '') { + return + } + + if (fetchingSearchResults) { + cancel() + } + + const searchParams = sidebarSearchQuery + .split(' ') + .reduce( + (params, str) => { + if (str === '--body') { + params.body = true + return params + } + if (str === '--title') { + params.title = true + return params + } + params.query = params.query == '' ? str : `${params.query} ${str}` + return params + }, + { query: '' } + ) + + // todo: implement search history for local space + // addToSearchHistory(searchParams.query) + await submitSearch({ query: searchParams }) + }, + 600, + [sidebarSearchQuery] + ) + + const [searchResults, setSearchResults] = useState([]) + const usersMap = new Map() + const [initialLoadDone] = useState(true) + const { + sideBarOpenedLinksIdsSet, + sideBarOpenedFolderIdsSet, + sideBarOpenedStorageIdsSet, + toggleItem, + unfoldItem, + foldItem, + } = useSidebarCollapse() + + const getFoldEvents = useCallback( + (type: CollapsableType, key: string) => { + return { + fold: () => foldItem(type, key), + unfold: () => unfoldItem(type, key), + toggle: () => toggleItem(type, key), + } + }, + [foldItem, unfoldItem, toggleItem] + ) + const { + updateFolder, + updateDocApi, + createFolder, + createDocApi, + deleteFolderApi, + toggleDocArchived, + toggleDocBookmark, + deleteStorageApi, + } = useLocalDB() + const { + openWorkspaceEditForm, + openNewDocForm, + openRenameFolderForm, + openRenameDocForm, + // deleteWorkspace, + } = useLocalUI() + const { draggedResource, dropInDocOrFolder, dropInWorkspace } = useLocalDnd() + const tree = useMemo(() => { + if (storage == null) { + return undefined + } + return mapTree( + initialLoadDone, + generalStatus.sidebarTreeSortingOrder, + storage, + storage.noteMap, + storage.folderMap, + storage.tagMap, + pathname, + sideBarOpenedLinksIdsSet, + sideBarOpenedFolderIdsSet, + sideBarOpenedStorageIdsSet, + toggleItem, + getFoldEvents, + push, + toggleDocBookmark, + deleteStorageApi, + toggleDocArchived, + deleteFolderApi, + createFolder, + createDocApi, + draggedResource, + dropInDocOrFolder, + (id: string) => dropInWorkspace(id, updateFolder, updateDocApi), + openRenameFolderForm, + openRenameDocForm, + openWorkspaceEditForm + ) + }, [ + initialLoadDone, + generalStatus.sidebarTreeSortingOrder, + storage, + pathname, + sideBarOpenedLinksIdsSet, + sideBarOpenedFolderIdsSet, + sideBarOpenedStorageIdsSet, + toggleItem, + getFoldEvents, + push, + toggleDocBookmark, + deleteStorageApi, + toggleDocArchived, + deleteFolderApi, + createFolder, + createDocApi, + draggedResource, + dropInDocOrFolder, + openRenameFolderForm, + openRenameDocForm, + openWorkspaceEditForm, + dropInWorkspace, + updateFolder, + updateDocApi, + ]) + + const activeBoostHubTeamDomain = useMemo(() => { + if (routeParams.name !== 'boosthub.teams.show') { + return null + } + return routeParams.domain + }, [routeParams]) + + const spaces = useMemo(() => { + const activeWorkspaceId: string | null = storage == null ? null : storage.id + const allSpaces: SidebarSpace[] = [] + const onSpaceLinkClick = ( + event: React.MouseEvent, + workspace: NoteStorage + ) => { + event.preventDefault() + navigate(workspace.id) + } + const onSpaceContextMenu = ( + event: React.MouseEvent, + workspace: NoteStorage + ) => { + event.preventDefault() + event.stopPropagation() + const menuItems: MenuItemConstructorOptions[] = [ + { + type: 'normal', + label: t('storage.rename'), + click: async () => { + openModal( + { + if (workspaceName == '') { + pushMessage({ + title: 'Cannot rename workspace', + description: 'Workspace name should not be empty.', + }) + closeLastModal() + return + } + await renameStorage(workspace.id, workspaceName) + closeLastModal() + }} + />, + { + showCloseIcon: true, + title: `Rename "${workspace.name}" storage`, + } + ) + }, + }, + { type: 'separator' }, + { + type: 'normal', + label: t('storage.remove'), + click: async () => { + messageBox({ + title: `Remove "${workspace.name}" storage`, + message: + workspace.type === 'fs' + ? "This operation won't delete the actual storage folder. You can add it to the app again." + : t('storage.removeMessage'), + iconType: DialogIconTypes.Warning, + // todo: [komediruzecki-22/05/2021] Test, maybe move to localUI and test remove.. + buttons: [ + { + label: t('storage.remove'), + onClick: () => { + removeStorage(workspace.id) + }, + }, + { label: t('general.cancel') }, + ], + }) + }, + }, + ] + openContextMenu({ menuItems }) + } + + localSpaces.forEach((workspace, index) => { + allSpaces.push( + mapLocalSpace( + workspace, + index, + activeWorkspaceId, + onSpaceLinkClick, + onSpaceContextMenu + ) + ) + }) + generalStatus.boostHubTeams.forEach((boostHubTeam, index) => { + allSpaces.push({ + label: boostHubTeam.name, + icon: boostHubTeam.iconUrl, + active: activeBoostHubTeamDomain === boostHubTeam.domain, + tooltip: `${osName === 'macos' ? '⌘' : 'Ctrl'} ${ + entries(storageMap).length + index + 1 + }`, + linkProps: { + onClick: (event) => { + event.preventDefault() + push(`/app/boosthub/teams/${boostHubTeam.domain}`) + }, + }, + }) + }) + + return allSpaces + }, [ + activeBoostHubTeamDomain, + closeLastModal, + generalStatus.boostHubTeams, + localSpaces, + messageBox, + navigate, + openModal, + push, + pushMessage, + removeStorage, + renameStorage, + storage, + storageMap, + t, + ]) + + const timelineRows = useMemo(() => { + if (storage == null) { + return [] + } + return mapTimelineItems(values(storage.noteMap), push, storage) + }, [push, storage]) + + const spaceBottomRows = useMemo(() => { + const rows: SidebarSpaceContentRow[] = [] + rows.push({ + label: 'Create Space', + icon: mdiPlus, + linkProps: { + onClick: (event) => { + event.preventDefault() + if (boostHubUserInfo == null) { + push('/app/boosthub/login') + } else { + push('/app/boosthub/teams') + } + }, + }, + }) + + if (boostHubUserInfo == null) { + rows.push({ + label: 'Sign in', + icon: mdiLogin, + linkProps: { + onClick: (event) => { + event.preventDefault() + + push('/app/boosthub/login') + }, + }, + }) + } else { + rows.push({ + label: 'Sign Out Team Account', + icon: mdiLogout, + linkProps: { + onClick: (event) => { + event.preventDefault() + signOut() + }, + }, + }) + } + + return rows + }, [boostHubUserInfo, push, signOut]) + + return ( + + setShowSpaces(false)} + toolbarRows={toolbarRows} + spaces={spaces} + spaceBottomRows={spaceBottomRows} + sidebarExpandedWidth={generalStatus.sideBarWidth} + sidebarState={sidebarState} + tree={tree} + sidebarResize={sidebarResize} + searchQuery={sidebarSearchQuery} + setSearchQuery={setSearchQuery} + // todo: add search history for local space (or use general search history when a shared component) + searchHistory={[]} + recentPages={historyItems} + treeControls={[ + { + icon: + generalStatus.sidebarTreeSortingOrder === 'a-z' + ? SidebarTreeSortingOrders.aZ.icon + : generalStatus.sidebarTreeSortingOrder === 'z-a' + ? SidebarTreeSortingOrders.zA.icon + : generalStatus.sidebarTreeSortingOrder === 'last-updated' + ? SidebarTreeSortingOrders.lastUpdated.icon + : SidebarTreeSortingOrders.dragDrop.icon, + onClick: (event) => { + popup( + event, + Object.values(SidebarTreeSortingOrders).map((sort) => { + return { + type: MenuTypes.Normal, + onClick: () => + setGeneralStatus({ + sidebarTreeSortingOrder: sort.value, + }), + label: sort.label, + icon: sort.icon, + active: + sort.value === generalStatus.sidebarTreeSortingOrder, + } + }) + ) + }, + }, + ]} + // todo: See why its not full width + treeTopRows={ + storage == null ? null : ( + + ) + } + searchResults={searchResults} + // todo: no users? + users={usersMap} + timelineRows={timelineRows} + timelineMore={ + storage != null + ? { + variant: 'primary', + onClick: () => push(getTimelineHref(storage)), + } + : undefined + } + sidebarSearchState={{ + fetching: fetchingSearchResults, + isNotDebouncing: isNotDebouncing() === true, + }} + /> + + ) +} + +export default SidebarContainer + +const NavigatorContainer = styled.nav` + //flex: 0 0 auto; + //min-width: 0; +` diff --git a/src/components/organisms/TagDetail.tsx b/src/components/organisms/TagDetail.tsx index 7d2c9ed48d..aceb8f3e09 100644 --- a/src/components/organisms/TagDetail.tsx +++ b/src/components/organisms/TagDetail.tsx @@ -5,14 +5,14 @@ import FolderDetailListNoteItem from '../molecules/FolderDetailListNoteItem' import { usePreferences } from '../../lib/preferences' import NoteSortingOptionsFragment from '../molecules/NoteSortingOptionsFragment' import { NoteSortingOptions } from '../../lib/sort' -import Icon from '../atoms/Icon' import { mdiTag } from '@mdi/js' -import styled from '../../lib/styled' +import styled from '../../shared/lib/styled' +import Icon from '../../shared/components/atoms/Icon' import { - flexCenter, borderBottom, + flexCenter, selectStyle, -} from '../../lib/styled/styleFunctions' +} from '../../shared/lib/styled/styleFunctions' interface TagDetailProps { storage: NoteStorage @@ -86,7 +86,7 @@ const TagDetail = ({ storage, tagName }: TagDetailProps) => { {tagName} -
    +