From faa09bd2bf158e9031b4610b893ffbb23916a21c Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 29 May 2024 13:37:18 +0700 Subject: [PATCH 01/15] feat: Dekstop Revamp (#2877) * feat: desktop revamp * feat: refactor system monitor * fix linter CI * remove unused import component * added responsive and resizeable component * responsive and resizeable local server page * finalize responsive and resizeable component * fix scroll custom ui * remove react scroll to bottom from modal troubleshoot * fix modal troubleshoot ui * fix setting gpu list * text area custom scroll bar * fix padding message input * cleanup classname * update inference engine model dropdown * update loader style * update quick ask ui * prepare theme provider * update dark theme * remove update hotkey list model and navigation * fix: cleanup hardcode classname * fix: update feedback * Set native theme electron * update destop ui revamp from feedback * update button icon component insider icon chat input message * update model dropdown ui * update tranaparent baclground * update logo model provider * fix: set background material acrylic support to blur background windows * fix: update tranparent left and right panel * fix: linter CI * update app using frameless window * styling custom style minimize, maximize and close app * temporary hidden maximize window * fix: responsive left and right panel * fix: enable click outside when leftpanel responsive * fix: remove unused import * update transparent variable css windows * fix: ui import model * feat: Support Theme system (#2946) * feat: update support theme system * update select component * feat: add theme folder in root project * fix: padding left and right center panel * fix: update padding left and right * chore: migrate themes * fix: rmdirsync error * chore: update gitignore * fix: cp recursive * fix: files electron package json * fix: migration * fix: update fgit ignore --------- Co-authored-by: Louis * fix: update feedback missing state when refrash app * fix: error test CI * chore: refactor useLoadThemes * chore: cleanup unused vars * fix: revert back menubar windows * fix minor ui * fix: minor ui --------- Co-authored-by: Louis --- .../jan-electron-linter-and-test.yml | 111 ++-- .gitignore | 1 + Dockerfile | 4 +- Dockerfile.gpu | 4 +- Makefile | 8 +- core/src/types/api/index.ts | 7 + core/src/types/model/modelEntity.ts | 5 + electron/handlers/native.ts | 52 +- electron/main.ts | 4 +- electron/managers/mainWindowConfig.ts | 18 +- electron/managers/window.ts | 18 +- electron/package.json | 10 +- electron/utils/menu.ts | 3 +- electron/utils/migration.ts | 49 +- electron/utils/setup.ts | 27 + {uikit => joi}/.prettierignore | 3 +- {uikit => joi}/.prettierrc | 0 joi/README.md | 13 + joi/package.json | 59 ++ joi/rollup.config.mjs | 73 +++ joi/src/core/Accordion/index.tsx | 45 ++ joi/src/core/Accordion/styles.scss | 73 +++ joi/src/core/Badge/index.tsx | 50 ++ joi/src/core/Badge/styles.scss | 131 +++++ joi/src/core/Button/index.tsx | 64 ++ joi/src/core/Button/styles.scss | 134 +++++ joi/src/core/Checkbox/index.tsx | 51 ++ joi/src/core/Checkbox/styles.scss | 51 ++ joi/src/core/Input/index.tsx | 46 ++ joi/src/core/Input/styles.scss | 43 ++ joi/src/core/Modal/index.tsx | 56 ++ joi/src/core/Modal/styles.scss | 85 +++ joi/src/core/Progress/index.tsx | 39 ++ joi/src/core/Progress/styles.scss | 25 + joi/src/core/ScrollArea/index.tsx | 35 ++ joi/src/core/ScrollArea/styles.scss | 69 +++ joi/src/core/Select/index.tsx | 85 +++ joi/src/core/Select/styles.scss | 77 +++ joi/src/core/Slider/index.tsx | 45 ++ joi/src/core/Slider/styles.scss | 38 ++ joi/src/core/Switch/index.tsx | 37 ++ joi/src/core/Switch/styles.scss | 67 +++ joi/src/core/Tabs/index.tsx | 59 ++ joi/src/core/Tabs/styles.scss | 37 ++ joi/src/core/TextArea/index.tsx | 24 + joi/src/core/TextArea/styles.scss | 54 ++ joi/src/core/Tooltip/index.tsx | 53 ++ joi/src/core/Tooltip/styles.scss | 82 +++ .../src/hooks/useClickOutside/index.tsx | 1 - joi/src/hooks/useClipboard/index.ts | 34 ++ joi/src/hooks/useMediaQuery/index.ts | 63 ++ joi/src/hooks/useOs/index.tsx | 56 ++ joi/src/hooks/usePageLeave/index.ts | 9 + joi/src/hooks/useTextSelection/index.ts | 24 + joi/src/index.ts | 21 + joi/tailwind.config.js | 10 + joi/tsconfig.json | 16 + package.json | 12 +- themes/dark-dimmed/theme.json | 144 +++++ themes/joi-dark/theme.json | 144 +++++ themes/joi-light/theme.json | 144 +++++ themes/night-blue/theme.json | 144 +++++ turbo.json | 4 +- uikit/package.json | 57 -- uikit/postcss.config.js | 8 - uikit/src/avatar/index.tsx | 43 -- uikit/src/avatar/styles.scss | 11 - uikit/src/badge/index.tsx | 32 - uikit/src/badge/styles.scss | 31 - uikit/src/button/index.tsx | 101 ---- uikit/src/button/styles.scss | 84 --- uikit/src/checkbox/index.tsx | 29 - uikit/src/checkbox/styles.scss | 7 - uikit/src/circular-progress/styles.scss | 66 --- uikit/src/command/index.tsx | 138 ----- uikit/src/command/styles.scss | 45 -- uikit/src/form/index.tsx | 175 ------ uikit/src/form/styles.scss | 21 - uikit/src/index.ts | 15 - uikit/src/input/index.tsx | 27 - uikit/src/input/styles.scss | 9 - uikit/src/main.scss | 111 ---- uikit/src/modal/index.tsx | 99 ---- uikit/src/modal/styles.scss | 32 - uikit/src/progress/index.tsx | 24 - uikit/src/progress/styles.scss | 7 - uikit/src/scroll-area/index.tsx | 51 -- uikit/src/scroll-area/styles.scss | 58 -- uikit/src/select/index.tsx | 136 ----- uikit/src/select/styles.scss | 34 -- uikit/src/slider/index.tsx | 25 - uikit/src/slider/styles.scss | 18 - uikit/src/switch/index.tsx | 22 - uikit/src/switch/styles.scss | 10 - uikit/src/textarea/index.tsx | 21 - uikit/src/textarea/styles.scss | 6 - uikit/src/tooltip/index.tsx | 43 -- uikit/src/tooltip/styles.scss | 6 - uikit/tailwind.config.js | 32 - uikit/tsconfig.json | 24 - uikit/types/declaration.d.ts | 4 - web/app/error.tsx | 20 +- web/app/layout.tsx | 3 +- web/app/privacy/page.tsx | 2 +- web/app/search/SelectedText.tsx | 10 +- web/app/search/UserInput.tsx | 6 +- web/app/search/layout.tsx | 2 +- web/app/search/page.tsx | 4 +- web/app/support/page.tsx | 2 +- web/constants/screens.ts | 1 - web/containers/AutoLink/index.tsx | 8 +- web/containers/CardSidebar/index.tsx | 187 ------ web/containers/CenterPanelContainer/index.tsx | 28 + web/containers/Checkbox/index.tsx | 41 +- web/containers/DropdownListSidebar/index.tsx | 354 ------------ .../EngineSetting/index.tsx | 20 +- .../ErrorMessage/index.tsx | 13 +- .../BottomBar/DownloadingState/index.tsx | 98 ---- .../InstallingExtensionModal.tsx | 87 --- .../BottomBar/UpdateFailedModal/index.tsx | 69 --- web/containers/Layout/BottomBar/index.tsx | 89 --- .../BottomPanel/DownloadingState/index.tsx | 97 ++++ .../ImportingModelState/index.tsx | 15 +- .../InstallingExtensionModal.tsx | 79 +++ .../InstallingExtension/index.tsx | 12 +- .../SystemMonitor/TableActiveModel/index.tsx | 60 +- .../SystemMonitor/index.tsx | 83 +-- .../Layout/BottomPanel/UpdateApp/index.tsx | 27 + .../BottomPanel/UpdateFailedModal/index.tsx | 59 ++ web/containers/Layout/BottomPanel/index.tsx | 49 ++ web/containers/Layout/Ribbon/index.tsx | 189 ------ web/containers/Layout/RibbonPanel/index.tsx | 120 ++++ .../CommandListDownloadedModel/index.tsx | 98 ---- .../Layout/TopBar/CommandSearch/index.tsx | 97 ---- web/containers/Layout/TopBar/index.tsx | 244 -------- web/containers/Layout/TopPanel/index.tsx | 120 ++++ web/containers/Layout/index.tsx | 87 +-- web/containers/LeftPanelContainer/index.tsx | 122 ++++ web/containers/ListContainer/index.tsx | 12 +- web/containers/Loader/GenerateResponse.tsx | 4 +- web/containers/Loader/ModelReload.tsx | 6 +- web/containers/Loader/ModelStart.tsx | 4 +- web/containers/Loader/index.tsx | 15 +- web/containers/LoadingModal/index.tsx | 19 +- web/containers/MainViewContainer/index.tsx | 10 +- web/containers/ModalCancelDownload/index.tsx | 60 +- web/containers/ModalTroubleShoot/AppLogs.tsx | 42 +- .../ModalTroubleShoot/DeviceSpecs.tsx | 34 +- web/containers/ModalTroubleShoot/index.tsx | 181 +++--- web/containers/ModelConfigInput/index.tsx | 40 +- web/containers/ModelDropdown/index.tsx | 434 ++++++++++++++ .../ModelLabel/NotEnoughMemoryLabel.tsx | 54 +- .../ModelLabel/RecommendedLabel.tsx | 22 +- .../ModelLabel/SlowOnYourDeviceLabel.tsx | 54 +- web/containers/ModelLabel/index.tsx | 12 +- .../ModelSearch/index.tsx} | 52 +- .../ModelSetting/SettingComponent.tsx | 0 .../ModelSetting/index.tsx | 4 +- web/containers/ProgressBar/index.tsx | 26 - .../Providers/AppUpdateListener.tsx | 9 +- web/containers/Providers/DataLoader.tsx | 2 + web/containers/Providers/Jotai.tsx | 3 +- web/containers/Providers/KeyListener.tsx | 33 +- web/containers/Providers/Responsive.tsx | 27 + web/containers/Providers/Theme.tsx | 32 +- web/containers/Providers/index.tsx | 37 +- web/containers/RightPanelContainer/index.tsx | 125 ++++ web/containers/ServerLogs/index.tsx | 129 +++-- web/containers/SetupRemoteModel/index.tsx | 91 +++ web/containers/Shortcut/index.tsx | 2 +- web/containers/ShortcutModal/index.tsx | 106 ---- web/containers/SliderRightPanel/index.tsx | 69 +-- web/containers/Toast/index.tsx | 124 ++-- web/helpers/atoms/ApiServer.atom.ts | 5 +- web/helpers/atoms/App.atom.ts | 7 + web/helpers/atoms/BottomPanel.atom.ts | 0 web/helpers/atoms/Extension.atom.ts | 7 + web/helpers/atoms/Model.atom.ts | 2 + web/helpers/atoms/Setting.atom.ts | 13 + web/helpers/atoms/Thread.atom.ts | 1 + web/helpers/atoms/ThreadRightPanel.atom.ts | 4 + web/hooks/useBodyClass.ts | 22 - web/hooks/useCreateNewThread.ts | 2 +- web/hooks/useLoadTheme.ts | 96 +++ web/hooks/usePath.ts | 3 +- web/hooks/useSendChatMessage.ts | 2 +- web/hooks/useUpdateModelParameters.ts | 3 +- web/hooks/useUserConfigs.ts | 11 - web/package.json | 2 +- web/public/images/ModelProvider/anthropic.svg | 9 + web/public/images/ModelProvider/cohere.svg | 30 + web/public/images/ModelProvider/martian.svg | 11 + web/public/images/ModelProvider/mistral.svg | 32 + web/public/images/ModelProvider/openai.svg | 24 + web/public/images/hub-banner.png | Bin 285174 -> 157760 bytes .../Chat/ChatBody/EmptyThread/index.tsx | 46 -- web/screens/Chat/ChatInput/index.tsx | 343 ----------- web/screens/Chat/CleanThreadModal/index.tsx | 65 --- web/screens/Chat/DeleteThreadModal/index.tsx | 68 --- .../Chat/Sidebar/AssistantTool/index.tsx | 196 ------- web/screens/Chat/ThreadList/index.tsx | 131 ----- .../ExploreModels/ExploreModelItem/index.tsx | 98 ---- web/screens/ExploreModels/index.tsx | 126 ---- .../ModelList/ModelHeader}/index.tsx | 74 +-- web/screens/Hub/ModelList/ModelItem/index.tsx | 100 ++++ .../ModelList}/index.tsx | 10 +- web/screens/Hub/index.tsx | 111 ++++ .../LocalServerCenterPanel/index.tsx | 84 +++ .../LocalServerLeftPanel/index.tsx | 263 +++++++++ .../LocalServerRightPanel/index.tsx | 135 +++++ web/screens/LocalServer/index.tsx | 545 +----------------- .../DataFolder/ModalChangeDirectory.tsx | 60 +- .../DataFolder/ModalConfirmDestNotEmpty.tsx | 53 +- .../DataFolder/ModalErrorSetDestGlobal.tsx | 45 +- .../DataFolder/ModalSameDirectory.tsx | 40 +- .../Settings/Advanced/DataFolder/index.tsx | 21 +- .../FactoryReset/ModalConfirmReset.tsx | 91 ++- .../Advanced/FactoryReset/ResettingModal.tsx | 18 +- .../Settings/Advanced/FactoryReset/index.tsx | 18 +- web/screens/Settings/Advanced/index.tsx | 368 ++++++------ .../Settings/Appearance/TogglePrimary.tsx | 57 -- .../Settings/Appearance/ToggleTheme.tsx | 40 -- web/screens/Settings/Appearance/index.tsx | 119 +++- .../Settings/CancelModelImportModal/index.tsx | 51 +- .../ChooseWhatToImportModal/index.tsx | 30 +- .../Settings/CoreExtensions/ExtensionItem.tsx | 97 ++-- web/screens/Settings/CoreExtensions/index.tsx | 266 ++++++--- .../Settings/EditModelInfoModal/index.tsx | 156 +++-- .../Settings/ExtensionSetting/index.tsx | 8 +- web/screens/Settings/Hotkeys/index.tsx | 77 +++ .../ModelDownloadList/index.tsx | 8 +- .../ModelDownloadRow/index.tsx | 22 +- .../ModelSegmentInfo/index.tsx | 36 +- .../HuggingFaceRepoDetailModal/index.tsx | 26 +- .../Settings/ImportInProgressIcon/index.tsx | 2 +- .../ImportModelOptionSelection.tsx | 19 +- .../Settings/ImportModelOptionModal/index.tsx | 50 +- .../Settings/ImportSuccessIcon/index.tsx | 6 +- .../ImportingModelItem.tsx | 10 +- .../Settings/ImportingModelModal/index.tsx | 64 +- web/screens/Settings/Models/Row.tsx | 205 ------- web/screens/Settings/Models/index.tsx | 111 ---- .../Settings/MyModels/MyModelList/index.tsx | 226 ++++++++ web/screens/Settings/MyModels/index.tsx | 147 +++++ .../Settings/SelectingModelModal/index.tsx | 71 ++- .../SettingDetailTextInputItem/index.tsx | 34 +- .../SettingDetailToggleItem/index.tsx | 17 +- .../SettingDetail/SettingDetailItem/index.tsx | 11 +- web/screens/Settings/SettingDetail/index.tsx | 20 +- .../SettingItem/index.tsx | 14 +- .../Settings/SettingLeftPanel/index.tsx | 130 +++++ web/screens/Settings/SettingMenu/index.tsx | 74 --- web/screens/Settings/index.tsx | 24 +- .../AssistantSetting/index.tsx | 2 +- .../ChatBody/EmptyModel/index.tsx | 14 +- .../ChatBody/EmptyThread/index.tsx | 44 ++ .../ThreadCenterPanel}/ChatBody/index.tsx | 6 +- .../ThreadCenterPanel/ChatInput/index.tsx | 453 +++++++++++++++ .../ThreadCenterPanel}/ChatItem/index.tsx | 2 +- .../EditChatInput/index.tsx | 52 +- .../FileUploadPreview/Icon.tsx | 0 .../FileUploadPreview/index.tsx | 10 +- .../ImageUploadPreview/index.tsx | 6 +- .../LoadModelError/index.tsx | 12 +- .../MessageToolbar/index.tsx | 37 +- .../RequestDownloadModel/index.tsx | 4 +- .../SimpleTextMessage/RelativeImage.tsx | 0 .../SimpleTextMessage/index.tsx | 78 +-- .../ThreadCenterPanel}/index.tsx | 82 ++- .../ModalCleanThread/index.tsx | 64 ++ .../ModalDeleteThread/index.tsx | 65 +++ web/screens/Thread/ThreadLeftPanel/index.tsx | 164 ++++++ .../PromptTemplateSetting/index.tsx | 2 +- .../Thread/ThreadRightPanel/Tools/index.tsx | 177 ++++++ .../ThreadRightPanel}/index.tsx | 203 +++---- web/screens/Thread/index.tsx | 16 + web/styles/base/global.scss | 12 +- web/styles/components/code-block.scss | 5 +- web/styles/components/loader.scss | 8 +- web/styles/components/message.scss | 13 +- web/styles/main.scss | 59 +- web/tailwind.config.js | 26 - web/types/appearance.d.ts | 6 - web/types/theme.d.ts | 8 + web/utils/componentSettings.ts | 2 +- web/utils/jsonToCssVariables.ts | 29 + .../predefinedComponent.ts | 0 287 files changed, 8712 insertions(+), 7660 deletions(-) rename {uikit => joi}/.prettierignore (75%) rename {uikit => joi}/.prettierrc (100%) create mode 100644 joi/README.md create mode 100644 joi/package.json create mode 100644 joi/rollup.config.mjs create mode 100644 joi/src/core/Accordion/index.tsx create mode 100644 joi/src/core/Accordion/styles.scss create mode 100644 joi/src/core/Badge/index.tsx create mode 100644 joi/src/core/Badge/styles.scss create mode 100644 joi/src/core/Button/index.tsx create mode 100644 joi/src/core/Button/styles.scss create mode 100644 joi/src/core/Checkbox/index.tsx create mode 100644 joi/src/core/Checkbox/styles.scss create mode 100644 joi/src/core/Input/index.tsx create mode 100644 joi/src/core/Input/styles.scss create mode 100644 joi/src/core/Modal/index.tsx create mode 100644 joi/src/core/Modal/styles.scss create mode 100644 joi/src/core/Progress/index.tsx create mode 100644 joi/src/core/Progress/styles.scss create mode 100644 joi/src/core/ScrollArea/index.tsx create mode 100644 joi/src/core/ScrollArea/styles.scss create mode 100644 joi/src/core/Select/index.tsx create mode 100644 joi/src/core/Select/styles.scss create mode 100644 joi/src/core/Slider/index.tsx create mode 100644 joi/src/core/Slider/styles.scss create mode 100644 joi/src/core/Switch/index.tsx create mode 100644 joi/src/core/Switch/styles.scss create mode 100644 joi/src/core/Tabs/index.tsx create mode 100644 joi/src/core/Tabs/styles.scss create mode 100644 joi/src/core/TextArea/index.tsx create mode 100644 joi/src/core/TextArea/styles.scss create mode 100644 joi/src/core/Tooltip/index.tsx create mode 100644 joi/src/core/Tooltip/styles.scss rename web/hooks/useClickOutside.ts => joi/src/hooks/useClickOutside/index.tsx (95%) create mode 100644 joi/src/hooks/useClipboard/index.ts create mode 100644 joi/src/hooks/useMediaQuery/index.ts create mode 100644 joi/src/hooks/useOs/index.tsx create mode 100644 joi/src/hooks/usePageLeave/index.ts create mode 100644 joi/src/hooks/useTextSelection/index.ts create mode 100644 joi/src/index.ts create mode 100644 joi/tailwind.config.js create mode 100644 joi/tsconfig.json create mode 100644 themes/dark-dimmed/theme.json create mode 100644 themes/joi-dark/theme.json create mode 100644 themes/joi-light/theme.json create mode 100644 themes/night-blue/theme.json delete mode 100644 uikit/package.json delete mode 100644 uikit/postcss.config.js delete mode 100644 uikit/src/avatar/index.tsx delete mode 100644 uikit/src/avatar/styles.scss delete mode 100644 uikit/src/badge/index.tsx delete mode 100644 uikit/src/badge/styles.scss delete mode 100644 uikit/src/button/index.tsx delete mode 100644 uikit/src/button/styles.scss delete mode 100644 uikit/src/checkbox/index.tsx delete mode 100644 uikit/src/checkbox/styles.scss delete mode 100644 uikit/src/circular-progress/styles.scss delete mode 100644 uikit/src/command/index.tsx delete mode 100644 uikit/src/command/styles.scss delete mode 100644 uikit/src/form/index.tsx delete mode 100644 uikit/src/form/styles.scss delete mode 100644 uikit/src/index.ts delete mode 100644 uikit/src/input/index.tsx delete mode 100644 uikit/src/input/styles.scss delete mode 100644 uikit/src/main.scss delete mode 100644 uikit/src/modal/index.tsx delete mode 100644 uikit/src/modal/styles.scss delete mode 100644 uikit/src/progress/index.tsx delete mode 100644 uikit/src/progress/styles.scss delete mode 100644 uikit/src/scroll-area/index.tsx delete mode 100644 uikit/src/scroll-area/styles.scss delete mode 100644 uikit/src/select/index.tsx delete mode 100644 uikit/src/select/styles.scss delete mode 100644 uikit/src/slider/index.tsx delete mode 100644 uikit/src/slider/styles.scss delete mode 100644 uikit/src/switch/index.tsx delete mode 100644 uikit/src/switch/styles.scss delete mode 100644 uikit/src/textarea/index.tsx delete mode 100644 uikit/src/textarea/styles.scss delete mode 100644 uikit/src/tooltip/index.tsx delete mode 100644 uikit/src/tooltip/styles.scss delete mode 100644 uikit/tailwind.config.js delete mode 100644 uikit/tsconfig.json delete mode 100644 uikit/types/declaration.d.ts delete mode 100644 web/containers/CardSidebar/index.tsx create mode 100644 web/containers/CenterPanelContainer/index.tsx delete mode 100644 web/containers/DropdownListSidebar/index.tsx rename web/{screens/Chat => containers}/EngineSetting/index.tsx (51%) rename web/{screens/Chat => containers}/ErrorMessage/index.tsx (90%) delete mode 100644 web/containers/Layout/BottomBar/DownloadingState/index.tsx delete mode 100644 web/containers/Layout/BottomBar/InstallingExtension/InstallingExtensionModal.tsx delete mode 100644 web/containers/Layout/BottomBar/UpdateFailedModal/index.tsx delete mode 100644 web/containers/Layout/BottomBar/index.tsx create mode 100644 web/containers/Layout/BottomPanel/DownloadingState/index.tsx rename web/containers/Layout/{BottomBar => BottomPanel}/ImportingModelState/index.tsx (79%) create mode 100644 web/containers/Layout/BottomPanel/InstallingExtension/InstallingExtensionModal.tsx rename web/containers/Layout/{BottomBar => BottomPanel}/InstallingExtension/index.tsx (79%) rename web/containers/Layout/{BottomBar => BottomPanel}/SystemMonitor/TableActiveModel/index.tsx (52%) rename web/containers/Layout/{BottomBar => BottomPanel}/SystemMonitor/index.tsx (60%) create mode 100644 web/containers/Layout/BottomPanel/UpdateApp/index.tsx create mode 100644 web/containers/Layout/BottomPanel/UpdateFailedModal/index.tsx create mode 100644 web/containers/Layout/BottomPanel/index.tsx delete mode 100644 web/containers/Layout/Ribbon/index.tsx create mode 100644 web/containers/Layout/RibbonPanel/index.tsx delete mode 100644 web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx delete mode 100644 web/containers/Layout/TopBar/CommandSearch/index.tsx delete mode 100644 web/containers/Layout/TopBar/index.tsx create mode 100644 web/containers/Layout/TopPanel/index.tsx create mode 100644 web/containers/LeftPanelContainer/index.tsx create mode 100644 web/containers/ModelDropdown/index.tsx rename web/{screens/Settings/Models/ModelSearch.tsx => containers/ModelSearch/index.tsx} (58%) rename web/{screens/Chat => containers}/ModelSetting/SettingComponent.tsx (100%) rename web/{screens/Chat => containers}/ModelSetting/index.tsx (91%) delete mode 100644 web/containers/ProgressBar/index.tsx create mode 100644 web/containers/Providers/Responsive.tsx create mode 100644 web/containers/RightPanelContainer/index.tsx create mode 100644 web/containers/SetupRemoteModel/index.tsx delete mode 100644 web/containers/ShortcutModal/index.tsx create mode 100644 web/helpers/atoms/BottomPanel.atom.ts create mode 100644 web/helpers/atoms/ThreadRightPanel.atom.ts delete mode 100644 web/hooks/useBodyClass.ts create mode 100644 web/hooks/useLoadTheme.ts delete mode 100644 web/hooks/useUserConfigs.ts create mode 100644 web/public/images/ModelProvider/anthropic.svg create mode 100644 web/public/images/ModelProvider/cohere.svg create mode 100644 web/public/images/ModelProvider/martian.svg create mode 100644 web/public/images/ModelProvider/mistral.svg create mode 100644 web/public/images/ModelProvider/openai.svg delete mode 100644 web/screens/Chat/ChatBody/EmptyThread/index.tsx delete mode 100644 web/screens/Chat/ChatInput/index.tsx delete mode 100644 web/screens/Chat/CleanThreadModal/index.tsx delete mode 100644 web/screens/Chat/DeleteThreadModal/index.tsx delete mode 100644 web/screens/Chat/Sidebar/AssistantTool/index.tsx delete mode 100644 web/screens/Chat/ThreadList/index.tsx delete mode 100644 web/screens/ExploreModels/ExploreModelItem/index.tsx delete mode 100644 web/screens/ExploreModels/index.tsx rename web/screens/{ExploreModels/ExploreModelItemHeader => Hub/ModelList/ModelHeader}/index.tsx (74%) create mode 100644 web/screens/Hub/ModelList/ModelItem/index.tsx rename web/screens/{ExploreModels/ExploreModelList => Hub/ModelList}/index.tsx (83%) create mode 100644 web/screens/Hub/index.tsx create mode 100644 web/screens/LocalServer/LocalServerCenterPanel/index.tsx create mode 100644 web/screens/LocalServer/LocalServerLeftPanel/index.tsx create mode 100644 web/screens/LocalServer/LocalServerRightPanel/index.tsx delete mode 100644 web/screens/Settings/Appearance/TogglePrimary.tsx delete mode 100644 web/screens/Settings/Appearance/ToggleTheme.tsx create mode 100644 web/screens/Settings/Hotkeys/index.tsx delete mode 100644 web/screens/Settings/Models/Row.tsx delete mode 100644 web/screens/Settings/Models/index.tsx create mode 100644 web/screens/Settings/MyModels/MyModelList/index.tsx create mode 100644 web/screens/Settings/MyModels/index.tsx rename web/screens/Settings/{SettingMenu => SettingLeftPanel}/SettingItem/index.tsx (59%) create mode 100644 web/screens/Settings/SettingLeftPanel/index.tsx delete mode 100644 web/screens/Settings/SettingMenu/index.tsx rename web/screens/{Chat => Thread/ThreadCenterPanel}/AssistantSetting/index.tsx (96%) rename web/screens/{Chat => Thread/ThreadCenterPanel}/ChatBody/EmptyModel/index.tsx (67%) create mode 100644 web/screens/Thread/ThreadCenterPanel/ChatBody/EmptyThread/index.tsx rename web/screens/{Chat => Thread/ThreadCenterPanel}/ChatBody/index.tsx (94%) create mode 100644 web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx rename web/screens/{Chat => Thread/ThreadCenterPanel}/ChatItem/index.tsx (87%) rename web/screens/{Chat => Thread/ThreadCenterPanel}/EditChatInput/index.tsx (85%) rename web/screens/{Chat => Thread/ThreadCenterPanel}/FileUploadPreview/Icon.tsx (100%) rename web/screens/{Chat => Thread/ThreadCenterPanel}/FileUploadPreview/index.tsx (82%) rename web/screens/{Chat => Thread/ThreadCenterPanel}/ImageUploadPreview/index.tsx (91%) rename web/screens/{Chat => Thread/ThreadCenterPanel}/LoadModelError/index.tsx (90%) rename web/screens/{Chat => Thread/ThreadCenterPanel}/MessageToolbar/index.tsx (77%) rename web/screens/{Chat => Thread/ThreadCenterPanel}/RequestDownloadModel/index.tsx (93%) rename web/screens/{Chat => Thread/ThreadCenterPanel}/SimpleTextMessage/RelativeImage.tsx (100%) rename web/screens/{Chat => Thread/ThreadCenterPanel}/SimpleTextMessage/index.tsx (81%) rename web/screens/{Chat => Thread/ThreadCenterPanel}/index.tsx (81%) create mode 100644 web/screens/Thread/ThreadLeftPanel/ModalCleanThread/index.tsx create mode 100644 web/screens/Thread/ThreadLeftPanel/ModalDeleteThread/index.tsx create mode 100644 web/screens/Thread/ThreadLeftPanel/index.tsx rename web/screens/{Chat/Sidebar => Thread/ThreadRightPanel}/PromptTemplateSetting/index.tsx (93%) create mode 100644 web/screens/Thread/ThreadRightPanel/Tools/index.tsx rename web/screens/{Chat/Sidebar => Thread/ThreadRightPanel}/index.tsx (50%) create mode 100644 web/screens/Thread/index.tsx delete mode 100644 web/types/appearance.d.ts create mode 100644 web/types/theme.d.ts create mode 100644 web/utils/jsonToCssVariables.ts rename web/{screens/Chat/ModelSetting => utils}/predefinedComponent.ts (100%) diff --git a/.github/workflows/jan-electron-linter-and-test.yml b/.github/workflows/jan-electron-linter-and-test.yml index 828162c57..3a95e804e 100644 --- a/.github/workflows/jan-electron-linter-and-test.yml +++ b/.github/workflows/jan-electron-linter-and-test.yml @@ -6,17 +6,17 @@ on: - main - dev paths: - - "electron/**" + - 'electron/**' - .github/workflows/jan-electron-linter-and-test.yml - - "web/**" - - "uikit/**" - - "package.json" - - "node_modules/**" - - "yarn.lock" - - "core/**" - - "extensions/**" - - "!README.md" - - "Makefile" + - 'web/**' + - 'joi/**' + - 'package.json' + - 'node_modules/**' + - 'yarn.lock' + - 'core/**' + - 'extensions/**' + - '!README.md' + - 'Makefile' pull_request: branches: @@ -24,17 +24,17 @@ on: - dev - release/** paths: - - "electron/**" + - 'electron/**' - .github/workflows/jan-electron-linter-and-test.yml - - "web/**" - - "uikit/**" - - "package.json" - - "node_modules/**" - - "yarn.lock" - - "Makefile" - - "extensions/**" - - "core/**" - - "!README.md" + - 'web/**' + - 'joi/**' + - 'package.json' + - 'node_modules/**' + - 'yarn.lock' + - 'Makefile' + - 'extensions/**' + - 'core/**' + - '!README.md' jobs: test-on-macos: @@ -51,23 +51,23 @@ jobs: with: node-version: 20 - - name: "Cleanup cache" + - name: 'Cleanup cache' continue-on-error: true run: | rm -rf ~/jan make clean - name: Get Commit Message for PR - if : github.event_name == 'pull_request' + if: github.event_name == 'pull_request' run: | echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}})" >> $GITHUB_ENV - name: Get Commit Message for push event - if : github.event_name == 'push' + if: github.event_name == 'push' run: | echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}})" >> $GITHUB_ENV - - name: "Config report portal" + - name: 'Config report portal' run: | make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App macos" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}" @@ -77,10 +77,10 @@ jobs: yarn config set registry ${{ secrets.NPM_PROXY }} --global make test env: - CSC_IDENTITY_AUTO_DISCOVERY: "false" - TURBO_API: "${{ secrets.TURBO_API }}" - TURBO_TEAM: "macos" - TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}" + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + TURBO_API: '${{ secrets.TURBO_API }}' + TURBO_TEAM: 'macos' + TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}' test-on-macos-pr-target: if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository @@ -96,7 +96,7 @@ jobs: with: node-version: 20 - - name: "Cleanup cache" + - name: 'Cleanup cache' continue-on-error: true run: | rm -rf ~/jan @@ -108,14 +108,14 @@ jobs: yarn config set registry https://registry.npmjs.org --global make test env: - CSC_IDENTITY_AUTO_DISCOVERY: "false" + CSC_IDENTITY_AUTO_DISCOVERY: 'false' test-on-windows: if: github.event_name == 'push' strategy: fail-fast: false matrix: - antivirus-tools: ['mcafee', 'default-windows-security','bit-defender'] + antivirus-tools: ['mcafee', 'default-windows-security', 'bit-defender'] runs-on: windows-desktop-${{ matrix.antivirus-tools }} steps: - name: Getting the repo @@ -129,7 +129,7 @@ jobs: node-version: 20 # Clean cache, continue on error - - name: "Cleanup cache" + - name: 'Cleanup cache' shell: powershell continue-on-error: true run: | @@ -140,14 +140,14 @@ jobs: Write-Output "Folder does not exist." } make clean - + - name: Get Commit Message for push event - if : github.event_name == 'push' + if: github.event_name == 'push' shell: bash run: | echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}}" >> $GITHUB_ENV - - name: "Config report portal" + - name: 'Config report portal' shell: bash run: | make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Windows ${{ matrix.antivirus-tools }}" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}" @@ -159,9 +159,9 @@ jobs: yarn config set registry ${{ secrets.NPM_PROXY }} --global make test env: - TURBO_API: "${{ secrets.TURBO_API }}" - TURBO_TEAM: "windows" - TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}" + TURBO_API: '${{ secrets.TURBO_API }}' + TURBO_TEAM: 'windows' + TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}' test-on-windows-pr: if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) runs-on: windows-desktop-default-windows-security @@ -177,7 +177,7 @@ jobs: node-version: 20 # Clean cache, continue on error - - name: "Cleanup cache" + - name: 'Cleanup cache' shell: powershell continue-on-error: true run: | @@ -190,12 +190,12 @@ jobs: make clean - name: Get Commit Message for PR - if : github.event_name == 'pull_request' + if: github.event_name == 'pull_request' shell: bash run: | echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}}" >> $GITHUB_ENV - - name: "Config report portal" + - name: 'Config report portal' shell: bash run: | make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Windows" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}" @@ -207,9 +207,9 @@ jobs: yarn config set registry ${{ secrets.NPM_PROXY }} --global make test env: - TURBO_API: "${{ secrets.TURBO_API }}" - TURBO_TEAM: "windows" - TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}" + TURBO_API: '${{ secrets.TURBO_API }}' + TURBO_TEAM: 'windows' + TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}' test-on-windows-pr-target: if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository @@ -226,7 +226,7 @@ jobs: node-version: 20 # Clean cache, continue on error - - name: "Cleanup cache" + - name: 'Cleanup cache' shell: powershell continue-on-error: true run: | @@ -245,7 +245,6 @@ jobs: yarn config set registry https://registry.npmjs.org --global make test - test-on-ubuntu: runs-on: [self-hosted, Linux, ubuntu-desktop] if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch' @@ -260,23 +259,23 @@ jobs: with: node-version: 20 - - name: "Cleanup cache" + - name: 'Cleanup cache' continue-on-error: true run: | rm -rf ~/jan make clean - name: Get Commit Message for PR - if : github.event_name == 'pull_request' + if: github.event_name == 'pull_request' run: | echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}}" >> $GITHUB_ENV - name: Get Commit Message for push event - if : github.event_name == 'push' + if: github.event_name == 'push' run: | echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}}" >> $GITHUB_ENV - - name: "Config report portal" + - name: 'Config report portal' shell: bash run: | make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Linux" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}" @@ -289,9 +288,9 @@ jobs: yarn config set registry ${{ secrets.NPM_PROXY }} --global make test env: - TURBO_API: "${{ secrets.TURBO_API }}" - TURBO_TEAM: "linux" - TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}" + TURBO_API: '${{ secrets.TURBO_API }}' + TURBO_TEAM: 'linux' + TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}' test-on-ubuntu-pr-target: runs-on: [self-hosted, Linux, ubuntu-desktop] @@ -307,16 +306,16 @@ jobs: with: node-version: 20 - - name: "Cleanup cache" + - name: 'Cleanup cache' continue-on-error: true run: | rm -rf ~/jan make clean - + - name: Linter and test run: | export DISPLAY=$(w -h | awk 'NR==1 {print $2}') echo -e "Display ID: $DISPLAY" npm config set registry https://registry.npmjs.org --global yarn config set registry https://registry.npmjs.org --global - make test \ No newline at end of file + make test diff --git a/.gitignore b/.gitignore index 1a7be6867..0b6f98465 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ electron/renderer electron/models electron/docs electron/engines +electron/themes electron/playwright-report server/pre-install package-lock.json diff --git a/Dockerfile b/Dockerfile index dee423170..7fbbda2cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,10 +39,10 @@ COPY --from=builder /app/docs/openapi ./docs/openapi/ COPY --from=builder /app/pre-install ./pre-install/ # Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache -COPY --from=builder /app/uikit ./uikit/ +COPY --from=builder /app/joi ./joi/ COPY --from=builder /app/web ./web/ -RUN yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build +RUN yarn workspace @janhq/joi install && yarn workspace @janhq/joi build RUN yarn workspace @janhq/web install RUN npm install -g serve@latest diff --git a/Dockerfile.gpu b/Dockerfile.gpu index 7adc1e02a..195a28d42 100644 --- a/Dockerfile.gpu +++ b/Dockerfile.gpu @@ -63,10 +63,10 @@ COPY --from=builder /app/docs/openapi ./docs/openapi/ COPY --from=builder /app/pre-install ./pre-install/ # Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache -COPY --from=builder /app/uikit ./uikit/ +COPY --from=builder /app/joi ./joi/ COPY --from=builder /app/web ./web/ -RUN yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build +RUN yarn workspace @janhq/joi install && yarn workspace @janhq/joi build RUN yarn workspace @janhq/web install RUN npm install -g serve@latest diff --git a/Makefile b/Makefile index a05f14c51..204e1698f 100644 --- a/Makefile +++ b/Makefile @@ -11,15 +11,15 @@ all: @echo "Specify a target to run" # Builds the UI kit -build-uikit: +build-joi: ifeq ($(OS),Windows_NT) - cd uikit && yarn config set network-timeout 300000 && yarn install && yarn build + cd joi && yarn config set network-timeout 300000 && yarn install && yarn build else - cd uikit && yarn install && yarn build + cd joi && yarn install && yarn build endif # Installs yarn dependencies and builds core and extensions -install-and-build: build-uikit +install-and-build: build-joi ifeq ($(OS),Windows_NT) yarn config set network-timeout 300000 endif diff --git a/core/src/types/api/index.ts b/core/src/types/api/index.ts index fb0dc5b93..971d5555b 100644 --- a/core/src/types/api/index.ts +++ b/core/src/types/api/index.ts @@ -11,6 +11,13 @@ export enum NativeRoute { selectDirectory = 'selectDirectory', selectFiles = 'selectFiles', relaunch = 'relaunch', + setNativeThemeLight = 'setNativeThemeLight', + setNativeThemeDark = 'setNativeThemeDark', + + setMinimizeApp = 'setMinimizeApp', + setCloseApp = 'setCloseApp', + setMaximizeApp = 'setMaximizeApp', + showOpenMenu = 'showOpenMenu', hideQuickAskWindow = 'hideQuickAskWindow', sendQuickAskInput = 'sendQuickAskInput', diff --git a/core/src/types/model/modelEntity.ts b/core/src/types/model/modelEntity.ts index 7b2828b46..426b30846 100644 --- a/core/src/types/model/modelEntity.ts +++ b/core/src/types/model/modelEntity.ts @@ -15,11 +15,16 @@ export type ModelInfo = { */ export enum InferenceEngine { + anthropic = 'anthropic', + mistral = 'mistral', + martian = 'martian', + openrouter = 'openrouter', nitro = 'nitro', openai = 'openai', groq = 'groq', triton_trtllm = 'triton_trtllm', nitro_tensorrt_llm = 'nitro-tensorrt-llm', + cohere = 'cohere', } export type ModelArtifact = { diff --git a/electron/handlers/native.ts b/electron/handlers/native.ts index 89bce15df..1bc815b41 100644 --- a/electron/handlers/native.ts +++ b/electron/handlers/native.ts @@ -1,4 +1,4 @@ -import { app, ipcMain, dialog, shell } from 'electron' +import { app, ipcMain, dialog, shell, nativeTheme, screen } from 'electron' import { join } from 'path' import { windowManager } from '../managers/window' import { @@ -10,7 +10,10 @@ import { NativeRoute, SelectFileProp, } from '@janhq/core/node' -import { SelectFileOption } from '@janhq/core/.' +import { SelectFileOption } from '@janhq/core' +import { menu } from '../utils/menu' + +const isMac = process.platform === 'darwin' export function handleAppIPCs() { /** @@ -22,6 +25,41 @@ export function handleAppIPCs() { shell.openPath(getJanDataFolderPath()) }) + /** + * Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light". + * This will change the appearance of the app to the light theme. + */ + ipcMain.handle(NativeRoute.setNativeThemeLight, () => { + nativeTheme.themeSource = 'light' + }) + + ipcMain.handle(NativeRoute.setCloseApp, () => { + windowManager.mainWindow?.close() + }) + + ipcMain.handle(NativeRoute.setMinimizeApp, () => { + windowManager.mainWindow?.minimize() + }) + + ipcMain.handle(NativeRoute.setMaximizeApp, async () => { + if (windowManager.mainWindow?.isMaximized()) { + // const bounds = await getBounds() + // windowManager.mainWindow?.setSize(bounds.width, bounds.height) + // windowManager.mainWindow?.setPosition(Number(bounds.x), Number(bounds.y)) + windowManager.mainWindow.restore() + } else { + windowManager.mainWindow?.maximize() + } + }) + + /** + * Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark". + * This will change the appearance of the app to the dark theme. + */ + ipcMain.handle(NativeRoute.setNativeThemeDark, () => { + nativeTheme.themeSource = 'dark' + }) + /** * Opens a URL in the user's default browser. * @param _event - The IPC event object. @@ -136,6 +174,16 @@ export function handleAppIPCs() { } ) + ipcMain.handle(NativeRoute.showOpenMenu, function (e, args) { + if (!isMac && windowManager.mainWindow) { + menu.popup({ + window: windowManager.mainWindow, + x: args.x, + y: args.y, + }) + } + }) + ipcMain.handle( NativeRoute.hideMainWindow, async (): Promise => windowManager.hideMainWindow() diff --git a/electron/main.ts b/electron/main.ts index 9f0bd8393..6ce7f476a 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -19,7 +19,7 @@ import { handleAppIPCs } from './handlers/native' **/ import { setupMenu } from './utils/menu' import { createUserSpace } from './utils/path' -import { migrateExtensions } from './utils/migration' +import { migrate } from './utils/migration' import { cleanUpAndQuit } from './utils/clean' import { setupExtensions } from './utils/extension' import { setupCore } from './utils/setup' @@ -79,7 +79,7 @@ app }) .then(setupCore) .then(createUserSpace) - .then(migrateExtensions) + .then(migrate) .then(setupExtensions) .then(setupMenu) .then(handleIPCs) diff --git a/electron/managers/mainWindowConfig.ts b/electron/managers/mainWindowConfig.ts index 990e78b85..25f0635f7 100644 --- a/electron/managers/mainWindowConfig.ts +++ b/electron/managers/mainWindowConfig.ts @@ -1,17 +1,17 @@ -const DEFAULT_WIDTH = 1200 const DEFAULT_MIN_WIDTH = 400 -const DEFAULT_HEIGHT = 800 export const mainWindowConfig: Electron.BrowserWindowConstructorOptions = { - width: DEFAULT_WIDTH, - minWidth: DEFAULT_MIN_WIDTH, - height: DEFAULT_HEIGHT, skipTaskbar: false, + minWidth: DEFAULT_MIN_WIDTH, show: true, + titleBarStyle: 'hidden', + vibrancy: 'fullscreen-ui', + visualEffectState: 'active', + backgroundMaterial: 'acrylic', + maximizable: false, + autoHideMenuBar: true, trafficLightPosition: { - x: 10, - y: 15, + x: 16, + y: 10, }, - titleBarStyle: 'hiddenInset', - vibrancy: 'sidebar', } diff --git a/electron/managers/window.ts b/electron/managers/window.ts index ab76bb94b..3d5107b28 100644 --- a/electron/managers/window.ts +++ b/electron/managers/window.ts @@ -2,12 +2,14 @@ import { BrowserWindow, app, shell } from 'electron' import { quickAskWindowConfig } from './quickAskWindowConfig' import { mainWindowConfig } from './mainWindowConfig' import { getAppConfigurations, AppEvent } from '@janhq/core/node' +import { getBounds, saveBounds } from '../utils/setup' /** * Manages the current window instance. */ // TODO: refactor this let isAppQuitting = false + class WindowManager { public mainWindow?: BrowserWindow private _quickAskWindow: BrowserWindow | undefined = undefined @@ -19,9 +21,15 @@ class WindowManager { * Creates a new window instance. * @returns The created window instance. */ - createMainWindow(preloadPath: string, startUrl: string) { + async createMainWindow(preloadPath: string, startUrl: string) { + const bounds = await getBounds() + this.mainWindow = new BrowserWindow({ ...mainWindowConfig, + width: bounds.width, + height: bounds.height, + x: bounds.x, + y: bounds.y, webPreferences: { nodeIntegration: true, preload: preloadPath, @@ -40,6 +48,14 @@ class WindowManager { } } + this.mainWindow.on('resized', () => { + saveBounds(this.mainWindow?.getBounds()) + }) + + this.mainWindow.on('moved', () => { + saveBounds(this.mainWindow?.getBounds()) + }) + /* Load frontend app to the window */ this.mainWindow.loadURL(startUrl) diff --git a/electron/package.json b/electron/package.json index 48b7eaee2..feaee5e16 100644 --- a/electron/package.json +++ b/electron/package.json @@ -14,15 +14,19 @@ "renderer/**/*", "build/**/*.{js,map}", "pre-install", + "themes", "docs/**/*", "scripts/**/*", - "icons/**/*" + "icons/**/*", + "themes" ], "asarUnpack": [ "pre-install", + "themes", "docs", "scripts", - "icons" + "icons", + "themes" ], "publish": [ { @@ -114,7 +118,7 @@ "@types/request": "^2.48.12", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", - "electron": "28.0.0", + "electron": "30.0.6", "electron-builder": "^24.13.3", "electron-builder-squirrel-windows": "^24.13.3", "electron-devtools-installer": "^3.2.0", diff --git a/electron/utils/menu.ts b/electron/utils/menu.ts index 893907c48..3f838e5ca 100644 --- a/electron/utils/menu.ts +++ b/electron/utils/menu.ts @@ -112,7 +112,8 @@ const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [ }, ] +export const menu = Menu.buildFromTemplate(template) + export const setupMenu = () => { - const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) } diff --git a/electron/utils/migration.ts b/electron/utils/migration.ts index 399b362f4..814247851 100644 --- a/electron/utils/migration.ts +++ b/electron/utils/migration.ts @@ -1,29 +1,42 @@ import { app } from 'electron' -import { rmdir } from 'fs' +import { join } from 'path' +import { rmdirSync, cpSync, existsSync } from 'fs' import Store from 'electron-store' -import { getJanExtensionsPath } from '@janhq/core/node' +import { + getJanExtensionsPath, + getJanDataFolderPath, + appResourcePath, +} from '@janhq/core/node' /** - * Migrates the extensions by deleting the `extensions` directory in the user data path. + * Migrates the extensions & themes. * If the `migrated_version` key in the `Store` object does not match the current app version, * the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version. * @returns A Promise that resolves when the migration is complete. */ -export function migrateExtensions() { - return new Promise((resolve) => { - const store = new Store() - if (store.get('migrated_version') !== app.getVersion()) { - console.debug('start migration:', store.get('migrated_version')) +export async function migrate() { + const store = new Store() + if (store.get('migrated_version') !== app.getVersion()) { + console.debug('start migration:', store.get('migrated_version')) - rmdir(getJanExtensionsPath(), { recursive: true }, function (err) { - if (err) console.error(err) - store.set('migrated_version', app.getVersion()) - console.debug('migrate extensions done') - resolve(undefined) - }) - } else { - resolve(undefined) - } - }) + if (existsSync(getJanExtensionsPath())) + rmdirSync(getJanExtensionsPath(), { recursive: true }) + await migrateThemes() + + store.set('migrated_version', app.getVersion()) + console.debug('migrate extensions done') + } else if (!existsSync(join(getJanDataFolderPath(), 'themes'))) { + await migrateThemes() + } +} + +async function migrateThemes() { + if (existsSync(join(getJanDataFolderPath(), 'themes'))) + rmdirSync(join(getJanDataFolderPath(), 'themes'), { recursive: true }) + cpSync( + join(await appResourcePath(), 'themes'), + join(getJanDataFolderPath(), 'themes'), + { recursive: true } + ) } diff --git a/electron/utils/setup.ts b/electron/utils/setup.ts index d60ab47bb..c53698351 100644 --- a/electron/utils/setup.ts +++ b/electron/utils/setup.ts @@ -1,4 +1,10 @@ import { app } from 'electron' +import Store from 'electron-store' + +const DEFAULT_WIDTH = 1000 +const DEFAULT_HEIGHT = 700 + +const storage = new Store() export const setupCore = async () => { // Setup core api for main process @@ -7,3 +13,24 @@ export const setupCore = async () => { appPath: () => app.getPath('userData'), } } + +export const getBounds = async () => { + const defaultBounds = { + x: undefined, + y: undefined, + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + } + + const bounds = await storage.get('windowBounds') + if (bounds) { + return bounds as Electron.Rectangle + } else { + storage.set('windowBounds', defaultBounds) + return defaultBounds + } +} + +export const saveBounds = (bounds: Electron.Rectangle | undefined) => { + storage.set('windowBounds', bounds) +} diff --git a/uikit/.prettierignore b/joi/.prettierignore similarity index 75% rename from uikit/.prettierignore rename to joi/.prettierignore index 02d9145c1..e9e840d7e 100644 --- a/uikit/.prettierignore +++ b/joi/.prettierignore @@ -2,4 +2,5 @@ node_modules/ dist/ *.hbs -*.mdx \ No newline at end of file +*.mdx +*.mjs \ No newline at end of file diff --git a/uikit/.prettierrc b/joi/.prettierrc similarity index 100% rename from uikit/.prettierrc rename to joi/.prettierrc diff --git a/joi/README.md b/joi/README.md new file mode 100644 index 000000000..161db4156 --- /dev/null +++ b/joi/README.md @@ -0,0 +1,13 @@ +# @janhq/joi + +To install dependencies: + +```bash +yarn install +``` + +To run: + +```bash +yarn run dev +``` diff --git a/joi/package.json b/joi/package.json new file mode 100644 index 000000000..3f1bd07f7 --- /dev/null +++ b/joi/package.json @@ -0,0 +1,59 @@ +{ + "name": "@janhq/joi", + "version": "0.0.0", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/index.d.ts", + "description": "A collection of UI component", + "private": true, + "files": [ + "dist" + ], + "keywords": [ + "design-system" + ], + "license": "MIT", + "homepage": "https://github.com/codecentrum/piksel#readme", + "repository": { + "type": "git", + "url": "https://github.com/codecentrum/piksel.git" + }, + "bugs": "https://github.com/codecentrum/piksel/issues", + "scripts": { + "dev": "rollup -c -w", + "build": "rimraf ./dist && rollup -c" + }, + "peerDependencies": { + "class-variance-authority": "^0.7.0", + "react": "^18", + "typescript": "^5.0.0" + }, + "dependencies": { + "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.1.2", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7", + "tailwind-merge": "^2.2.0", + "autoprefixer": "10.4.16", + "tailwindcss": "^3.4.1" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", + "prettier": "^3.0.3", + "prettier-plugin-tailwindcss": "^0.5.6", + "rollup": "^4.12.0", + "rollup-plugin-bundle-size": "^1.0.3", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-copy": "^3.5.0", + "rollup-plugin-dts": "^6.1.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-postcss": "^4.0.2", + "rollup-plugin-typescript2": "^0.36.0" + } +} diff --git a/joi/rollup.config.mjs b/joi/rollup.config.mjs new file mode 100644 index 000000000..333a61c5c --- /dev/null +++ b/joi/rollup.config.mjs @@ -0,0 +1,73 @@ +import { readFileSync } from 'fs' +import dts from 'rollup-plugin-dts' +import terser from '@rollup/plugin-terser' +import autoprefixer from 'autoprefixer' +import commonjs from 'rollup-plugin-commonjs' +import bundleSize from 'rollup-plugin-bundle-size' +import peerDepsExternal from 'rollup-plugin-peer-deps-external' +import postcss from 'rollup-plugin-postcss' +import typescript from 'rollup-plugin-typescript2' +import tailwindcss from 'tailwindcss' +import typescriptEngine from 'typescript' +import resolve from '@rollup/plugin-node-resolve' +import copy from 'rollup-plugin-copy' + +const packageJson = JSON.parse(readFileSync('./package.json')) + +import tailwindConfig from './tailwind.config.js' + +export default [ + { + input: `./src/index.ts`, + output: [ + { + file: packageJson.main, + format: 'cjs', + sourcemap: false, + exports: 'named', + name: packageJson.name, + }, + { + file: packageJson.module, + format: 'es', + exports: 'named', + sourcemap: false, + }, + ], + plugins: [ + postcss({ + plugins: [autoprefixer(), tailwindcss(tailwindConfig)], + sourceMap: true, + use: ['sass'], + minimize: true, + extract: 'main.css', + }), + peerDepsExternal({ includeDependencies: true }), + resolve(), + commonjs(), + typescript({ + tsconfig: './tsconfig.json', + typescript: typescriptEngine, + sourceMap: false, + exclude: ['docs', 'dist', 'node_modules/**'], + }), + terser(), + ], + watch: { + clearScreen: false, + }, + }, + { + input: 'dist/esm/index.d.ts', + output: [{ file: 'dist/index.d.ts', format: 'esm' }], + external: [/\.(sc|sa|c)ss$/], + plugins: [ + dts(), + peerDepsExternal({ includeDependencies: true }), + copy({ + targets: [{ src: 'dist/esm/main.css', dest: 'dist' }], + }), + bundleSize(), + ], + }, +] diff --git a/joi/src/core/Accordion/index.tsx b/joi/src/core/Accordion/index.tsx new file mode 100644 index 000000000..75a671ca4 --- /dev/null +++ b/joi/src/core/Accordion/index.tsx @@ -0,0 +1,45 @@ +import React, { ReactNode } from 'react' +import * as AccordionPrimitive from '@radix-ui/react-accordion' + +import { ChevronDownIcon } from '@radix-ui/react-icons' + +import './styles.scss' + +type AccordionProps = { + defaultValue: string[] + children: ReactNode +} + +type AccordionItemProps = { + children: ReactNode + value: string + title: string +} + +const AccordionItem = ({ children, value, title }: AccordionItemProps) => { + return ( + + + +
{title}
+ +
+
+ +
{children}
+
+
+ ) +} + +const Accordion = ({ defaultValue, children }: AccordionProps) => ( + + {children} + +) + +export { Accordion, AccordionItem } diff --git a/joi/src/core/Accordion/styles.scss b/joi/src/core/Accordion/styles.scss new file mode 100644 index 000000000..028cc021c --- /dev/null +++ b/joi/src/core/Accordion/styles.scss @@ -0,0 +1,73 @@ +.accordion { + border-top: 1px solid hsla(var(--app-border)); + + &__item { + overflow: hidden; + margin-top: 1px; + border-bottom: 1px solid hsla(var(--app-border)); + + :focus-within { + position: relative; + z-index: 1; + } + } + + &__header { + display: flex; + } + + &__trigger { + font-family: inherit; + background-color: transparent; + padding: 0 16px; + height: 40px; + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 500; + } + + &__content { + overflow: hidden; + + &--wrapper { + padding: 4px 16px 16px 16px; + } + } + + &__chevron { + color: hsla(var(--text-secondary)); + transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1); + } +} + +.accordion__content[data-state='open'] { + animation: slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1); +} + +.accordion__content[data-state='closed'] { + animation: slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1); +} + +.accordion__trigger[data-state='open'] > .accordion__chevron { + transform: rotate(180deg); +} + +@keyframes slideDown { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } +} + +@keyframes slideUp { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } +} diff --git a/joi/src/core/Badge/index.tsx b/joi/src/core/Badge/index.tsx new file mode 100644 index 000000000..ffc34624f --- /dev/null +++ b/joi/src/core/Badge/index.tsx @@ -0,0 +1,50 @@ +import React, { HTMLAttributes } from 'react' + +import { cva, type VariantProps } from 'class-variance-authority' + +import { twMerge } from 'tailwind-merge' + +import './styles.scss' + +const badgeVariants = cva('badge', { + variants: { + theme: { + primary: 'badge--primary', + secondary: 'badge--secondary', + warning: 'badge--warning', + success: 'badge--success', + info: 'badge--info', + destructive: 'badge--destructive', + }, + variant: { + solid: 'badge--solid', + soft: 'badge--soft', + outline: 'badge--outline', + }, + size: { + small: 'badge--small', + medium: 'badge--medium', + large: 'badge--large', + }, + }, + defaultVariants: { + theme: 'primary', + size: 'medium', + variant: 'solid', + }, +}) + +export interface BadgeProps + extends HTMLAttributes, + VariantProps {} + +const Badge = ({ className, theme, size, variant, ...props }: BadgeProps) => { + return ( +
+ ) +} + +export { Badge } diff --git a/joi/src/core/Badge/styles.scss b/joi/src/core/Badge/styles.scss new file mode 100644 index 000000000..a912e9216 --- /dev/null +++ b/joi/src/core/Badge/styles.scss @@ -0,0 +1,131 @@ +.badge { + @apply inline-flex items-center justify-center px-2 font-medium transition-all; + + // Primary + &--primary { + color: hsla(var(--primary-fg)); + background-color: hsla(var(--primary-bg)); + + // Variant soft primary + &.badge--soft { + background-color: hsla(var(--primary-bg-soft)); + color: hsla(var(--primary-bg)); + } + + // Variant outline primary + &.badge--outline { + background-color: transparent; + border: 1px solid hsla(var(--primary-bg)); + color: hsla(var(--primary-bg)); + } + } + + // Secondary + &--secondary { + background-color: hsla(var(--secondary-bg)); + color: hsla(var(--secondary-fg)); + + &.badge--soft { + background-color: hsla(var(--secondary-bg-soft)); + color: hsla(var(--secondary-bg)); + } + + // Variant outline secondary + &.badge--outline { + background-color: transparent; + border: 1px solid hsla(var(--secondary-bg)); + } + } + + // Destructive + &--destructive { + color: hsla(var(--destructive-fg)); + background-color: hsla(var(--destructive-bg)); + + // Variant soft destructive + &.badge--soft { + background-color: hsla(var(--destructive-bg-soft)); + color: hsla(var(--destructive-bg)); + } + + // Variant outline destructive + &.badge--outline { + background-color: transparent; + border: 1px solid hsla(var(--destructive-bg)); + color: hsla(var(--destructive-bg)); + } + } + + // Success + &--success { + @apply text-white; + background-color: hsla(var(--success-bg)); + + // Variant soft success + &.badge--soft { + background-color: hsla(var(--success-bg-soft)); + color: hsla(var(--success-bg)); + } + + // Variant outline success + &.badge--outline { + background-color: transparent; + border: 1px solid hsla(var(--success-bg)); + color: hsla(var(--success-bg)); + } + } + + // Warning + &--warning { + @apply text-white; + background-color: hsla(var(--warning-bg)); + + // Variant soft warning + &.badge--soft { + background-color: hsla(var(--warning-bg-soft)); + color: hsla(var(--warning-bg)); + } + + // Variant outline warning + &.badge--outline { + background-color: transparent; + border: 1px solid hsla(var(--warning-bg)); + color: hsla(var(--warning-bg)); + } + } + + // Info + &--info { + @apply text-white; + background-color: hsla(var(--info-bg)); + + // Variant soft info + &.badge--soft { + background-color: hsla(var(--info-bg-soft)); + color: hsla(var(--info-bg)); + } + + // Variant outline info + &.badge--outline { + background-color: transparent; + border: 1px solid hsla(var(--info-bg)); + color: hsla(var(--info-bg)); + } + } + + // Size + &--small { + @apply h-5; + border-radius: 4px; + } + + &--medium { + @apply h-6; + border-radius: 6px; + } + + &--large { + @apply h-7; + border-radius: 8px; + } +} diff --git a/joi/src/core/Button/index.tsx b/joi/src/core/Button/index.tsx new file mode 100644 index 000000000..014f534b0 --- /dev/null +++ b/joi/src/core/Button/index.tsx @@ -0,0 +1,64 @@ +import React, { forwardRef, ButtonHTMLAttributes } from 'react' + +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { twMerge } from 'tailwind-merge' + +import './styles.scss' + +const buttonVariants = cva('btn', { + variants: { + theme: { + primary: 'btn--primary', + ghost: 'btn--ghost', + icon: 'btn--icon', + destructive: 'btn--destructive', + }, + variant: { + solid: 'btn--solid', + soft: 'btn--soft', + outline: 'btn--outline', + }, + size: { + small: 'btn--small', + medium: 'btn--medium', + large: 'btn--large', + }, + block: { + true: 'btn--block', + }, + }, + defaultVariants: { + theme: 'primary', + size: 'medium', + variant: 'solid', + block: false, + }, +}) + +export interface ButtonProps + extends ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = forwardRef( + ( + { className, theme, size, variant, block, asChild = false, ...props }, + ref + ) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) + +export { Button } diff --git a/joi/src/core/Button/styles.scss b/joi/src/core/Button/styles.scss new file mode 100644 index 000000000..f7cdce6a4 --- /dev/null +++ b/joi/src/core/Button/styles.scss @@ -0,0 +1,134 @@ +.btn { + @apply inline-flex items-center justify-center px-4 font-semibold transition-all; + + &:focus, + &:focus-within { + @apply outline-2 outline-offset-4; + } + &:hover { + filter: brightness(95%); + } + + // Primary + &--primary { + color: hsla(var(--primary-fg)); + background-color: hsla(var(--primary-bg)) !important; + &:hover { + filter: brightness(65%); + } + + // Variant soft primary + &.btn--soft { + background-color: hsla(var(--primary-bg-soft)) !important; + color: hsla(var(--primary-bg)); + } + + // Variant outline primary + &.btn--outline { + background-color: transparent !important; + border: 1px solid hsla(var(--primary-bg)); + color: hsla(var(--primary-bg)); + } + } + + // Ghost + &--ghost { + background-color: transparent !important; + &.btn--soft { + background-color: transparent !important; + } + + // Variant outline ghost + &.btn--outline { + background-color: transparent !important; + border: 1px solid hsla(var(--ghost-border)); + } + } + + // Destructive + &--destructive { + color: hsla(var(--destructive-fg)); + background-color: hsla(var(--destructive-bg)) !important; + &:hover { + filter: brightness(65%); + } + + // Variant soft destructive + &.btn--soft { + background-color: hsla(var(--destructive-bg-soft)) !important; + color: hsla(var(--destructive-bg)); + } + + // Variant outline destructive + &.btn--outline { + background-color: transparent !important; + border: 1px solid hsla(var(--destructive-bg)); + color: hsla(var(--destructive-bg)); + } + } + + // Disabled + &:disabled { + color: hsla(var(--disabled-fg)); + background-color: hsla(var(--disabled-bg)) !important; + cursor: not-allowed; + + &:hover { + filter: brightness(100%); + } + } + + // Icon + &--icon { + width: 24px; + height: 24px; + padding: 2px; + &:hover { + background-color: hsla(var(--icon-bg)) !important; + } + + &.btn--outline { + background-color: transparent !important; + border: 1px solid hsla(var(--icon-border)); + &:hover { + background-color: hsla(var(--icon-bg)) !important; + } + } + } + + // Size + &--small { + @apply h-6 px-2; + font-size: 12px; + border-radius: 4px; + &.btn--icon { + width: 24px; + height: 24px; + padding: 2px; + } + } + + &--medium { + @apply h-8; + border-radius: 6px; + &.btn--icon { + width: 24px; + height: 24px; + padding: 2px; + } + } + + &--large { + @apply h-9; + border-radius: 8px; + &.btn--icon { + width: 24px; + height: 24px; + padding: 2px; + } + } + + &--block { + @apply w-full; + } +} diff --git a/joi/src/core/Checkbox/index.tsx b/joi/src/core/Checkbox/index.tsx new file mode 100644 index 000000000..71f9523ac --- /dev/null +++ b/joi/src/core/Checkbox/index.tsx @@ -0,0 +1,51 @@ +import React, { ChangeEvent, InputHTMLAttributes, ReactNode } from 'react' + +import { twMerge } from 'tailwind-merge' + +import './styles.scss' + +export interface CheckboxProps extends InputHTMLAttributes { + disabled?: boolean + className?: string + label?: ReactNode + helperDescription?: ReactNode + errorMessage?: string + onChange?: (e: ChangeEvent) => void +} + +const Checkbox = ({ + id, + name, + checked, + disabled, + label, + defaultChecked, + helperDescription, + errorMessage, + className, + onChange, + ...props +}: CheckboxProps) => { + return ( +
+ +
+ +

{helperDescription}

+ {errorMessage &&

{errorMessage}

} +
+
+ ) +} +export { Checkbox } diff --git a/joi/src/core/Checkbox/styles.scss b/joi/src/core/Checkbox/styles.scss new file mode 100644 index 000000000..775a6289b --- /dev/null +++ b/joi/src/core/Checkbox/styles.scss @@ -0,0 +1,51 @@ +.checkbox { + @apply inline-flex items-start space-x-2; + + > input[type='checkbox'] { + @apply flex h-4 w-4 flex-shrink-0 cursor-pointer appearance-none items-center justify-center; + background-color: transparent; + margin-top: 1px; + border: 1px solid hsla(var(--app-border)); + border-radius: 4px; + &:focus, + &:focus-within { + @apply outline-2 outline-offset-4; + } + + &:checked { + background-color: hsla(var(--primary-bg)); + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E"); + } + + &:disabled { + background-color: hsla(var(----disabled-bg)); + color: hsla(var(--disabled-fg)); + + &:checked { + background-color: hsla(var(--primary-bg)); + @apply cursor-not-allowed opacity-50; + } + + & + div > .checkbox__label { + @apply cursor-not-allowed opacity-50; + } + } + } + + &__helper { + font-size: 12px; + } + + &__error { + color: hsla(var(--destructive-bg)); + } + + &__label { + @apply inline-block cursor-pointer; + } + + &:disabled { + background-color: hsla(var(----disabled-bg)); + color: hsla(var(--disabled-fg)); + } +} diff --git a/joi/src/core/Input/index.tsx b/joi/src/core/Input/index.tsx new file mode 100644 index 000000000..d82099e9c --- /dev/null +++ b/joi/src/core/Input/index.tsx @@ -0,0 +1,46 @@ +import React, { ReactNode, forwardRef } from 'react' +import { twMerge } from 'tailwind-merge' + +import './styles.scss' + +export interface Props extends React.InputHTMLAttributes { + textAlign?: 'left' | 'right' + prefixIcon?: ReactNode + suffixIcon?: ReactNode + onCLick?: () => void +} + +const Input = forwardRef( + ( + { className, type, textAlign, prefixIcon, suffixIcon, onClick, ...props }, + ref + ) => { + return ( +
+ {prefixIcon && ( +
+ {prefixIcon} +
+ )} + {suffixIcon && ( +
+ {suffixIcon} +
+ )} + +
+ ) + } +) + +export { Input } diff --git a/joi/src/core/Input/styles.scss b/joi/src/core/Input/styles.scss new file mode 100644 index 000000000..a6226fa06 --- /dev/null +++ b/joi/src/core/Input/styles.scss @@ -0,0 +1,43 @@ +.input { + background-color: hsla(var(--input-bg)); + border: 1px solid hsla(var(--app-border)); + @apply inline-flex h-8 w-full items-center rounded-md border px-3 transition-colors; + @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-1 focus-visible:ring-[hsla(var(--primary-bg))] focus-visible:ring-offset-0; + @apply file:border-0 file:bg-transparent file:font-medium; + @apply hover:border-[hsla(var(--primary-bg))]; + + &__wrapper { + position: relative; + } + + &.text-right { + text-align: right; + } + + &::placeholder { + color: hsla(var(--input-placeholder)); + } + + &:disabled { + color: hsla(var(--disabled-fg)); + background-color: hsla(var(--disabled-bg)); + cursor: not-allowed; + border: none; + } + + &__prefix-icon { + @apply absolute left-3 top-1/2 -translate-y-1/2 cursor-pointer; + color: hsla(var(--input-icon)); + + .input { + padding-left: 32px; + } + } + + &__suffix-icon { + @apply absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer; + color: hsla(var(--input-icon)); + + .input { + padding-right: 32px; + } + } +} diff --git a/joi/src/core/Modal/index.tsx b/joi/src/core/Modal/index.tsx new file mode 100644 index 000000000..923004b99 --- /dev/null +++ b/joi/src/core/Modal/index.tsx @@ -0,0 +1,56 @@ +import React, { ReactNode } from 'react' +import * as DialogPrimitive from '@radix-ui/react-dialog' +import { Cross2Icon } from '@radix-ui/react-icons' + +import './styles.scss' +import { twMerge } from 'tailwind-merge' + +type Props = { + trigger?: ReactNode + content: ReactNode + open?: boolean + className?: string + fullPage?: boolean + hideClose?: boolean + title?: ReactNode + onOpenChange?: (open: boolean) => void +} + +const ModalClose = DialogPrimitive.Close + +const Modal = ({ + trigger, + content, + open, + title, + fullPage, + className, + onOpenChange, + hideClose, +}: Props) => ( + + {trigger} + + + +
{title}
+ {content} + {!hideClose && ( + + + + )} +
+
+
+) + +export { Modal, ModalClose } diff --git a/joi/src/core/Modal/styles.scss b/joi/src/core/Modal/styles.scss new file mode 100644 index 000000000..8baddaef2 --- /dev/null +++ b/joi/src/core/Modal/styles.scss @@ -0,0 +1,85 @@ +/* reset */ +button, +fieldset, +.modal { + &__overlay { + @apply backdrop-blur-lg; + background-color: hsla(var(--modal-overlay)); + z-index: 200; + position: fixed; + inset: 0; + animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1); + } + + &__content { + color: hsla(var(--modal-fg)); + overflow: hidden; + background-color: hsla(var(--modal-bg)); + border-radius: 8px; + position: fixed; + z-index: 300; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 50vw; + max-width: 560px; + max-height: 85vh; + padding: 16px; + animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1); + border: 1px solid hsla(var(--app-border)); + @apply w-full; + + &--fullpage { + max-width: none; + width: 90vw; + max-height: 90vh; + } + + &:focus { + outline: none; + } + } + + &__title { + @apply line-clamp-1; + margin: 0 0 8px 0; + padding-right: 16px; + font-weight: 600; + color: hsla(var(--modal-fg)); + font-size: 18px; + } + + &__close-icon { + font-family: inherit; + border-radius: 100%; + height: 24px; + width: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + color: hsla(var(--modal-fg)); + position: absolute; + top: 8px; + right: 16px; + } +} + +@keyframes overlayShow { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes contentShow { + from { + opacity: 0; + transform: translate(-50%, -48%) scale(0.96); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} diff --git a/joi/src/core/Progress/index.tsx b/joi/src/core/Progress/index.tsx new file mode 100644 index 000000000..51ea79c81 --- /dev/null +++ b/joi/src/core/Progress/index.tsx @@ -0,0 +1,39 @@ +import React, { HTMLAttributes } from 'react' + +import { cva, type VariantProps } from 'class-variance-authority' + +import { twMerge } from 'tailwind-merge' + +import './styles.scss' + +const progressVariants = cva('progress', { + variants: { + size: { + small: 'progress--small', + medium: 'progress--medium', + large: 'progress--large', + }, + }, + defaultVariants: { + size: 'medium', + }, +}) + +export interface ProgressProps + extends HTMLAttributes, + VariantProps { + value: number +} + +const Progress = ({ className, size, value, ...props }: ProgressProps) => { + return ( +
+
+
+ ) +} + +export { Progress } diff --git a/joi/src/core/Progress/styles.scss b/joi/src/core/Progress/styles.scss new file mode 100644 index 000000000..02d22f5f4 --- /dev/null +++ b/joi/src/core/Progress/styles.scss @@ -0,0 +1,25 @@ +.progress { + background-color: hsla(var(--progress-track-bg)); + border-radius: 8px; + position: relative; + overflow: hidden; + @apply transition-all; + + &--indicator { + background-color: hsla(var(--primary-bg)); + position: absolute; + border-radius: 8px; + width: 100%; + height: 100%; + } + + &--small { + height: 6px; + } + &--medium { + @apply h-2; + } + &--large { + @apply h-3; + } +} diff --git a/joi/src/core/ScrollArea/index.tsx b/joi/src/core/ScrollArea/index.tsx new file mode 100644 index 000000000..abe2adda9 --- /dev/null +++ b/joi/src/core/ScrollArea/index.tsx @@ -0,0 +1,35 @@ +import React, { PropsWithChildren, forwardRef } from 'react' +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area' +import { twMerge } from 'tailwind-merge' + +import './styles.scss' + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + + + + + + +)) + +export { ScrollArea } diff --git a/joi/src/core/ScrollArea/styles.scss b/joi/src/core/ScrollArea/styles.scss new file mode 100644 index 000000000..cb5832c53 --- /dev/null +++ b/joi/src/core/ScrollArea/styles.scss @@ -0,0 +1,69 @@ +.scroll-area { + position: relative; + z-index: 999; + + &__root { + width: 200px; + height: 225px; + overflow: hidden; + } + + &__viewport { + width: 100%; + height: 100%; + border-radius: inherit; + } + + &__bar { + display: flex; + user-select: none; + touch-action: none; + padding: 1px; + background: hsla(var(--scrollbar-tracker)); + transition: background 160ms ease-out; + } + + &__thumb { + flex: 1; + background: hsla(var(--scrollbar-thumb)); + border-radius: 20px; + position: relative; + + ::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + height: 100%; + min-width: 44px; + min-height: 44px; + } + } +} + +.scroll-area__bar[data-orientation='vertical'] { + width: 8px; +} + +.scroll-area__bar[data-orientation='horizontal'] { + flex-direction: column; + height: 8px; +} + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track, +::-webkit-scrollbar-thumb { + background-clip: content-box; + border-radius: inherit; +} +::-webkit-scrollbar-track { + background: hsla(var(--scrollbar-tracker)); +} +::-webkit-scrollbar-thumb { + background: hsla(var(--scrollbar-thumb)); +} diff --git a/joi/src/core/Select/index.tsx b/joi/src/core/Select/index.tsx new file mode 100644 index 000000000..bce5473da --- /dev/null +++ b/joi/src/core/Select/index.tsx @@ -0,0 +1,85 @@ +import React, { ReactNode } from 'react' + +import * as SelectPrimitive from '@radix-ui/react-select' +import { + CheckIcon, + ChevronDownIcon, + ChevronUpIcon, +} from '@radix-ui/react-icons' + +import './styles.scss' +import { twMerge } from 'tailwind-merge' + +type Props = { + options?: { name: string; value: string }[] + open?: boolean + block?: boolean + value?: string + placeholder?: string + disabled?: boolean + containerPortal?: HTMLDivElement | undefined | null + className?: string + onValueChange?: (value: string) => void + onOpenChange?: (open: boolean) => void +} + +const Select = ({ + placeholder, + options, + value, + disabled, + containerPortal, + block, + className, + open, + onValueChange, + onOpenChange, +}: Props) => ( + + + + + + + + + + + + {options && + options.map((item, i) => { + return ( + + + {item.name} + + + + + + ) + })} + + + + + +) + +export { Select } diff --git a/joi/src/core/Select/styles.scss b/joi/src/core/Select/styles.scss new file mode 100644 index 000000000..573833890 --- /dev/null +++ b/joi/src/core/Select/styles.scss @@ -0,0 +1,77 @@ +.select { + padding: 0 16px; + background-color: hsla(var(--select-input-bg)) !important; + border: 1px solid hsla(var(--app-border)); + @apply inline-flex h-8 items-center justify-between gap-8 rounded-md px-3 transition-colors; + @apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-1 focus-visible:ring-[hsla(var(--primary-bg))] focus-visible:ring-offset-0; + @apply text-sm hover:border-[hsla(var(--primary-bg))]; + + &[data-placeholder] { + color: hsla(var(--select-placeholder)); + } + + &__icon { + color: hsla(var(--select-icon)); + } + + &__content { + overflow: hidden; + background-color: hsla(var(--select-bg)); + z-index: 999; + border: 1px solid hsla(var(--select-border)); + border-radius: 8px; + box-shadow: + 0px 10px 38px -10px rgba(22, 23, 24, 0.35), + 0px 10px 20px -15px rgba(22, 23, 24, 0.2); + } + + &__viewport { + } + + &__disabled { + cursor: not-allowed; + pointer-events: none; + background-color: hsla(var(--disabled-bg)) !important; + color: hsla(var(--disabled-fg)); + border: none; + } + + &__item { + display: flex; + align-items: center; + padding: 8px 32px 8px 16px; + position: relative; + cursor: pointer; + @apply text-sm; + + &:hover { + background-color: hsla(var(--select-options-active-bg)); + } + + &[data-disabled] { + pointer-events: none; + } + + &[data-highlighted] { + outline: none; + } + } + + &__item-indicator { + position: absolute; + right: 0; + width: 25px; + display: inline-flex; + align-items: center; + justify-content: center; + } + + &__scroll-button { + display: flex; + align-items: center; + justify-content: center; + height: 25px; + background-color: white; + cursor: default; + } +} diff --git a/joi/src/core/Slider/index.tsx b/joi/src/core/Slider/index.tsx new file mode 100644 index 000000000..40e0c3977 --- /dev/null +++ b/joi/src/core/Slider/index.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import * as SliderPrimitive from '@radix-ui/react-slider' + +import './styles.scss' + +type Props = { + name?: string + min?: number + max?: number + onValueChange?(value: number[]): void + value?: number[] + defaultValue?: number[] + step?: number + disabled?: boolean +} + +const Slider = ({ + name, + min, + max, + onValueChange, + value, + defaultValue, + step, + disabled, +}: Props) => ( + + + + + + +) + +export { Slider } diff --git a/joi/src/core/Slider/styles.scss b/joi/src/core/Slider/styles.scss new file mode 100644 index 000000000..019e5ba38 --- /dev/null +++ b/joi/src/core/Slider/styles.scss @@ -0,0 +1,38 @@ +.slider { + position: relative; + display: flex; + align-items: center; + user-select: none; + touch-action: none; + height: 16px; + + &__track { + background-color: hsla(var(--slider-track-bg)); + position: relative; + flex-grow: 1; + border-radius: 9999px; + height: 4px; + } + + &__range { + position: absolute; + background-color: hsla(var(--primary-bg)); + border-radius: 9999px; + height: 100%; + } + + &__thumb { + display: block; + width: 16px; + height: 16px; + background-color: hsla(var(--slider-thumb-bg)); + border-radius: 10px; + padding: 2px; + border: 2px solid hsla(var(--primary-bg)); + + &:focus { + outline: none; + box-shadow: 0 0 0 5px hsla(var(--slider-track-bg), 50%); + } + } +} diff --git a/joi/src/core/Switch/index.tsx b/joi/src/core/Switch/index.tsx new file mode 100644 index 000000000..28eabe6e6 --- /dev/null +++ b/joi/src/core/Switch/index.tsx @@ -0,0 +1,37 @@ +import React, { ChangeEvent, InputHTMLAttributes } from 'react' + +import { twMerge } from 'tailwind-merge' + +import './styles.scss' + +export interface SwitchProps extends InputHTMLAttributes { + disabled?: boolean + className?: string + onChange?: (e: ChangeEvent) => void +} + +const Switch = ({ + name, + checked, + disabled, + defaultChecked, + className, + onChange, + ...props +}: SwitchProps) => { + return ( + + ) +} +export { Switch } diff --git a/joi/src/core/Switch/styles.scss b/joi/src/core/Switch/styles.scss new file mode 100644 index 000000000..9f7adbd4f --- /dev/null +++ b/joi/src/core/Switch/styles.scss @@ -0,0 +1,67 @@ +.switch { + position: relative; + display: inline-block; + width: 32px; + height: 18px; + + > input { + opacity: 0; + width: 0; + height: 0; + + // disabled + &:disabled { + + .switch--thumb { + cursor: not-allowed; + background-color: hsla(var(--disabled-bg)); + &:before { + background-color: hsla(var(--disabled-fg)); + } + } + // disabled and checked + &:checked + .switch--thumb { + cursor: not-allowed; + background-color: hsla(var(--primary-bg)); + &:before { + background-color: hsla(var(--disabled-fg)); + } + } + } + + &:checked + .switch--thumb { + background-color: hsla(var(--primary-bg)); + + &::before { + -webkit-transform: translateX(14px); + -ms-transform: translateX(14px); + transform: translateX(14px); + } + } + } + + &--thumb { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: hsla(var(--switch-bg)); + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 20px; + + &:before { + position: absolute; + content: ''; + height: 14px; + width: 14px; + left: 2px; + bottom: 2px; + background-color: hsla(var(--switch-fg)); + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 50%; + } + } +} diff --git a/joi/src/core/Tabs/index.tsx b/joi/src/core/Tabs/index.tsx new file mode 100644 index 000000000..edec179f1 --- /dev/null +++ b/joi/src/core/Tabs/index.tsx @@ -0,0 +1,59 @@ +import React, { ReactNode } from 'react' + +import * as TabsPrimitive from '@radix-ui/react-tabs' + +import './styles.scss' + +type TabsProps = { + options: { name: string; value: string }[] + children: ReactNode + defaultValue?: string + value: string + onValueChange?: (value: string) => void +} + +type TabsContentProps = { + value: string + children: ReactNode +} + +const TabsContent = ({ value, children }: TabsContentProps) => { + return ( + + {children} + + ) +} + +const Tabs = ({ + options, + children, + defaultValue, + value, + onValueChange, +}: TabsProps) => ( + + + {options.map((option, i) => { + return ( + + {option.name} + + ) + })} + + + {children} + +) + +export { Tabs, TabsContent } diff --git a/joi/src/core/Tabs/styles.scss b/joi/src/core/Tabs/styles.scss new file mode 100644 index 000000000..86948ab5a --- /dev/null +++ b/joi/src/core/Tabs/styles.scss @@ -0,0 +1,37 @@ +.tabs { + display: flex; + flex-direction: column; + width: 100%; + + &__list { + flex-shrink: 0; + display: flex; + border-bottom: 1px solid hsla(var(--app-border)); + } + + &__trigger { + padding: 0 12px; + flex: 1; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + user-select: none; + &:focus { + position: relative; + } + } + + &__content { + flex-grow: 1; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + outline: none; + } +} + +.tabs__trigger[data-state='active'] { + border-bottom: 1px solid hsla(var(--primary-bg)); + font-weight: 600; +} diff --git a/joi/src/core/TextArea/index.tsx b/joi/src/core/TextArea/index.tsx new file mode 100644 index 000000000..33d6744ad --- /dev/null +++ b/joi/src/core/TextArea/index.tsx @@ -0,0 +1,24 @@ +import React, { ReactNode, forwardRef } from 'react' +import { twMerge } from 'tailwind-merge' + +import './styles.scss' +import { ScrollArea } from '../ScrollArea' + +export interface TextAreaProps + extends React.TextareaHTMLAttributes {} + +const TextArea = forwardRef( + ({ className, ...props }, ref) => { + return ( +
+