diff --git a/public/locales/en.json b/public/locales/en.json index 9a80bc5..7b23443 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -13,6 +13,7 @@ "search": { "placeholder": "Search the Krist network", "placeholderShortcut": "Search the Krist network ({{shortcut}})", + "placeholderShort": "Search...", "rateLimitHit": "Please slow down.", "noResults": "No results.", @@ -40,7 +41,8 @@ "send": "Send", "request": "Request", - "settings": "Settings" + "settings": "Settings", + "more": "More" }, "sidebar": { diff --git a/src/layout/AppLayout.tsx b/src/layout/AppLayout.tsx index 6b9161e..0669b02 100644 --- a/src/layout/AppLayout.tsx +++ b/src/layout/AppLayout.tsx @@ -8,6 +8,8 @@ import { Sidebar } from "./sidebar/Sidebar"; import { AppRouter } from "../global/AppRouter"; +import { TopMenuProvider } from "./nav/TopMenu"; + import "./AppLayout.less"; const { useBreakpoint } = Grid; @@ -17,20 +19,25 @@ const bps = useBreakpoint(); return - + + - - + + - {/* Fade out the background when the sidebar is open on mobile */} - {!bps.md &&
setSidebarCollapsed(true)} - />} + {/* Fade out the background when the sidebar is open on mobile */} + {!bps.md &&
setSidebarCollapsed(true)} + />} - - + + + - + ; } diff --git a/src/layout/PageLayout.less b/src/layout/PageLayout.less index e1f79c7..29e98da 100644 --- a/src/layout/PageLayout.less +++ b/src/layout/PageLayout.less @@ -16,12 +16,45 @@ color: inherit; } } + + // Hide extra table pagination on mobile + @media (max-width: @screen-md) { + .ant-pagination { + display: none; + } + } } .page-layout-contents { height: calc(100% - @kw-page-header-height); padding: @padding-lg; + + // Make tables full-width on mobile (though most should be replaced with + // custom views) + @media (max-width: @screen-md) { + >.ant-table-wrapper { + .ant-table { + margin: 0 -24px; + } + + .ant-pagination { + justify-content: center; + + .ant-pagination-total-text { + display: none; + } + + .ant-pagination-item, .ant-pagination-prev{ + margin-right: 3px; + } + + .ant-pagination-next { + margin-right: 0; + } + } + } + } } &.page-layout-no-top-padding { diff --git a/src/layout/nav/AppHeader.less b/src/layout/nav/AppHeader.less index fb4b0ff..07dffb5 100644 --- a/src/layout/nav/AppHeader.less +++ b/src/layout/nav/AppHeader.less @@ -168,8 +168,47 @@ .site-header-settings { border-left: 1px solid @kw-border-color-darker; - .anticon { - margin-right: 0; + // Make the whole button clickable, especially for mobile + >.ant-menu-item { + padding: 0; + + .anticon { + padding: 0 20px; + margin-right: 0; + } + } + } +} + +.ant-dropdown.site-header-top-dropdown-menu { + // Make the top menu dropdown full-width and slightly larger on mobile + @media (max-width: @screen-md) { + position: absolute; + + top: @layout-header-height !important; + left: 0 !important; + right: 0 !important; + + border-radius: 0; + + .ant-dropdown-menu-item { + padding: 8px 16px; + font-size: @font-size-base; + + // If there's a direct div child, it's probably a descendent of an + // AuthorisedAction. Grow it to be full-width, so the click trigger is + // correct. + // FIXME: This is a hack. + >div { + margin: -8px -16px; + padding: 8px 16px; + } + + .anticon:first-child { + margin-right: 16px; + font-size: @font-size-base; + vertical-align: -0.175em; + } } } } diff --git a/src/layout/nav/AppHeader.tsx b/src/layout/nav/AppHeader.tsx index 96a6f1f..67a1b40 100644 --- a/src/layout/nav/AppHeader.tsx +++ b/src/layout/nav/AppHeader.tsx @@ -2,7 +2,7 @@ // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import { Layout, Menu, Grid } from "antd"; -import { SendOutlined, DownloadOutlined, MenuOutlined, SettingOutlined } from "@ant-design/icons"; +import { SendOutlined, DownloadOutlined, MenuOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; @@ -10,6 +10,7 @@ import { Search } from "./Search"; import { ConnectionIndicator } from "./ConnectionIndicator"; import { CymbalIndicator } from "./CymbalIndicator"; +import { TopMenu } from "./TopMenu"; import { ConditionalLink } from "@comp/ConditionalLink"; @@ -61,11 +62,7 @@ {/* Cymbal indicator */} - {/* Settings button */} - - } title={t("nav.settings")}> - - - + {/* Settings button, or dropdown on mobile if there are other options */} + ; } diff --git a/src/layout/nav/Search.tsx b/src/layout/nav/Search.tsx index 2cfc7bf..907eaa3 100644 --- a/src/layout/nav/Search.tsx +++ b/src/layout/nav/Search.tsx @@ -2,7 +2,7 @@ // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import { useState, useMemo, useRef, useEffect, useCallback, MutableRefObject, Dispatch, SetStateAction, ReactNode } from "react"; -import { AutoComplete, Input } from "antd"; +import { AutoComplete, Input, Grid } from "antd"; import { RefSelectProps } from "antd/lib/select"; import { useTranslation } from "react-i18next"; @@ -55,6 +55,9 @@ const { t } = useTranslation(); const history = useHistory(); + // Used to change the placeholder depending on the screen width + const bps = Grid.useBreakpoint(); + const [value, setValue] = useState(""); const [results, setResults] = useState(); const [extendedResults, setExtendedResults] = useState(); @@ -394,7 +397,9 @@ options={options} > void; + +interface TopMenuCtxRes { + options?: ReactNode; + setMenuOptions?: SetMenuOptsFn; +} + +export const TopMenuContext = createContext({}); + +export function TopMenu(): JSX.Element { + const { tStr } = useTFns("nav."); + const bps = Grid.useBreakpoint(); + + const ctxRes = useContext(TopMenuContext); + const options = ctxRes?.options; + + const menu = useMemo(() => ( + + {options} + + + + {/* Settings item */} + }> + {tStr("settings")} + + } + > + + + ), [tStr, options]); + + // If on mobile and there are options available from the page, display them + // instead of the settings button. + const btn = useMemo(() => ( + // Menu or settings button in the header + + {!bps.md && options + ? ( + // Menu button, if there are options available + + {menu} + + ) + : ( + // Regular settings button + } title={tStr("settings")}> + + + )} + + ), [tStr, bps, options, menu]); + + return btn; +} + +export const TopMenuProvider: FC = ({ children }) => { + const [menuOptions, setMenuOptions] = useState(); + const res: TopMenuCtxRes = useMemo(() => ({ + options: menuOptions, setMenuOptions + }), [menuOptions, setMenuOptions]); + + return + {children} + ; +}; + +export function useTopMenuOptions(): [boolean, SetMenuOptsFn, () => void] { + const bps = Grid.useBreakpoint(); + const { setMenuOptions } = useContext(TopMenuContext); + + const set = useCallback((opts: Opts) => { + debug("top menu options hook set"); + setMenuOptions?.(opts); + }, [setMenuOptions]); + + const unset = useCallback(() => { + debug("top menu options hook destructor"); + setMenuOptions?.(undefined); + }, [setMenuOptions]); + + // Return whether or not the options are being shown + return [!bps.md, set, unset]; +} diff --git a/src/pages/wallets/AddWalletModal.tsx b/src/pages/wallets/AddWalletModal.tsx index f926324..ffd46aa 100644 --- a/src/pages/wallets/AddWalletModal.tsx +++ b/src/pages/wallets/AddWalletModal.tsx @@ -259,7 +259,7 @@ > {/* Wallet label */} - + {/* Wallet category */} - + form.setFieldsValue({ category })} diff --git a/src/pages/wallets/ManageBackupsDropdown.tsx b/src/pages/wallets/ManageBackupsDropdown.tsx index 1ce7ce0..2419468 100644 --- a/src/pages/wallets/ManageBackupsDropdown.tsx +++ b/src/pages/wallets/ManageBackupsDropdown.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import { useState } from "react"; +import { Dispatch, SetStateAction} from "react"; import { Button, Dropdown, Menu } from "antd"; import { DatabaseOutlined, DownOutlined, ImportOutlined, ExportOutlined @@ -12,15 +12,17 @@ import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; import { useMasterPassword } from "@wallets"; -import { ImportBackupModal } from "../backup/ImportBackupModal"; -import { ExportBackupModal } from "../backup/ExportBackupModal"; +interface Props { + setImportVisible: Dispatch>; + setExportVisible: Dispatch>; +} -export function ManageBackupsDropdown(): JSX.Element { +export function ManageBackupsDropdown({ + setImportVisible, + setExportVisible +}: Props): JSX.Element { const { tStr } = useTFns("myWallets."); - const [importVisible, setImportVisible] = useState(false); - const [exportVisible, setExportVisible] = useState(false); - // Used to disable the export button if a master password hasn't been set up const { hasMasterPassword, salt, tester } = useMasterPassword(); const allowExport = !!hasMasterPassword && !!salt && !!tester; @@ -49,8 +51,5 @@ {tStr("manageBackups")} - - - ; } diff --git a/src/pages/wallets/WalletsPage.tsx b/src/pages/wallets/WalletsPage.tsx index 33e9392..e211a43 100644 --- a/src/pages/wallets/WalletsPage.tsx +++ b/src/pages/wallets/WalletsPage.tsx @@ -1,19 +1,13 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import { useState } from "react"; -import { Button } from "antd"; -import { PlusOutlined } from "@ant-design/icons"; - import { useTranslation } from "react-i18next"; import { PageLayout } from "@layout/PageLayout"; -import { AuthorisedAction } from "@comp/auth/AuthorisedAction"; import { useWallets } from "@wallets"; -import { ManageBackupsDropdown } from "./ManageBackupsDropdown"; -import { AddWalletModal } from "./AddWalletModal"; +import { useWalletsPageActions } from "./WalletsPageActions"; import { WalletsTable } from "./WalletsTable"; import { useEditWalletModal } from "./WalletEditButton"; @@ -34,40 +28,17 @@ }; } -function WalletsPageExtraButtons(): JSX.Element { - const { t } = useTranslation(); - const [createWalletVisible, setCreateWalletVisible] = useState(false); - const [addWalletVisible, setAddWalletVisible] = useState(false); - - return <> - {/* Manage backups */} - - - {/* Create wallet */} - setCreateWalletVisible(true)}> - - - - - {/* Add existing wallet */} - setAddWalletVisible(true)}> - - - - ; -} - export function WalletsPage(): JSX.Element { const [openEditWallet, editWalletModal] = useEditWalletModal(); const [openSendTx, sendTxModal] = useSendTransactionModal(); const [openWalletInfo, walletInfoModal] = useWalletInfoModal(); + const extra = useWalletsPageActions(); + return } - extra={} + extra={extra} > >; + setAddWalletVisible: Dispatch>; + setImportVisible: Dispatch>; + setExportVisible: Dispatch>; +} + +function WalletsPageExtraButtons({ + setCreateWalletVisible, + setAddWalletVisible, + setImportVisible, + setExportVisible +}: ExtraButtonsProps): JSX.Element { + const { tStr } = useTFns("myWallets."); + + return <> + {/* Manage backups */} + + + {/* Create wallet */} + setCreateWalletVisible(true)}> + + + + {/* Add existing wallet */} + setAddWalletVisible(true)}> + + + ; +} + +export function useWalletsPageActions(): JSX.Element | null { + const { tStr } = useTFns("myWallets."); + + const [createWalletVisible, setCreateWalletVisible] = useState(false); + const [addWalletVisible, setAddWalletVisible] = useState(false); + + const [importVisible, setImportVisible] = useState(false); + const [exportVisible, setExportVisible] = useState(false); + + // Used to disable the export button if a master password hasn't been set up + const { hasMasterPassword, salt, tester } = useMasterPassword(); + const allowExport = !!hasMasterPassword && !!salt && !!tester; + + const [usingTopMenu, set, unset] = useTopMenuOptions(); + useEffect(() => { + set(<> + {/* Create wallet */} + + setCreateWalletVisible(true)}> +
{tStr("createWallet")}
+
+
+ + {/* Add existing wallet */} + + setAddWalletVisible(true)}> +
{tStr("addExistingWallet")}
+
+
+ + {/* Backups */} + + + {/* Import backup */} + + setImportVisible(true)}> +
{tStr("importBackup")}
+
+
+ + {/* Export backup */} + setExportVisible(true)}> + {tStr("exportBackup")} + + ); + return unset; + }, [tStr, set, unset, allowExport]); + + return <> + {/* Only display the buttons on desktop */} + {!usingTopMenu && } + + + + + + ; +} diff --git a/src/style/components.less b/src/style/components.less index c350a36..9af38ff 100644 --- a/src/style/components.less +++ b/src/style/components.less @@ -224,13 +224,24 @@ height: 100%; } -// Make all icons in menu consistent (sometimes the icon may not be a direct -// descendent of the menu item) -.ant-dropdown-menu-item .anticon:first-child { - min-width: 12px; - margin-right: 8px; - font-size: @font-size-sm; - vertical-align: -0.1em; +.ant-dropdown-menu-item { + // Make all icons in menu consistent (sometimes the icon may not be a direct + // descendent of the menu item) + .anticon:first-child { + min-width: 12px; + margin-right: 8px; + font-size: @font-size-sm; + vertical-align: -0.1em; + } + + // If there's a direct div child, it's probably a descendent of an + // AuthorisedAction. Grow it to be full-width, so the click trigger is + // correct. + // FIXME: This is a hack. + >div { + margin: -5px -12px; + padding: 5px 12px; + } } .table-actions {