diff --git a/client/common/icons.jsx b/client/common/icons.jsx index cc979d3d..d4a458bc 100644 --- a/client/common/icons.jsx +++ b/client/common/icons.jsx @@ -17,6 +17,12 @@ import Code from '../images/code.svg'; import Save from '../images/save.svg'; import Terminal from '../images/terminal.svg'; +import Folder from '../images/folder-padded.svg'; + +import CircleTerminal from '../images/circle-terminal.svg'; +import CircleFolder from '../images/circle-folder.svg'; +import CircleInfo from '../images/circle-info.svg'; + // HOC that adds the right web accessibility props // https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html @@ -83,3 +89,9 @@ export const MoreIcon = withLabel(More); export const TerminalIcon = withLabel(Terminal); export const CodeIcon = withLabel(Code); export const SaveIcon = withLabel(Save); + +export const FolderIcon = withLabel(Folder); + +export const CircleTerminalIcon = withLabel(CircleTerminal); +export const CircleFolderIcon = withLabel(CircleFolder); +export const CircleInfoIcon = withLabel(CircleInfo); diff --git a/client/components/Dropdown.jsx b/client/components/Dropdown.jsx index bd2169b8..48bed0cb 100644 --- a/client/components/Dropdown.jsx +++ b/client/components/Dropdown.jsx @@ -25,7 +25,7 @@ const DropdownWrapper = styled.ul` display: flex; flex-direction: column; height: auto; - z-index: 9999; + z-index: 2; border-radius: ${remSize(6)}; & li:first-child { border-radius: ${remSize(5)} ${remSize(5)} 0 0; } diff --git a/client/components/mobile/ActionStrip.jsx b/client/components/mobile/ActionStrip.jsx index a93a6607..7486d1e5 100644 --- a/client/components/mobile/ActionStrip.jsx +++ b/client/components/mobile/ActionStrip.jsx @@ -1,23 +1,34 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; -import { remSize } from '../../theme'; +import { remSize, prop } from '../../theme'; import IconButton from './IconButton'; const BottomBarContent = styled.div` + padding: ${remSize(8)}; display: grid; grid-template-columns: repeat(8,1fr); - padding: ${remSize(8)}; - + svg { max-height: ${remSize(32)}; } + + path { fill: ${prop('primaryTextColor')} !important } + + .inverted { + path { fill: ${prop('backgroundColor')} !important } + rect { fill: ${prop('primaryTextColor')} !important } + } `; const ActionStrip = ({ actions }) => ( - {actions.map(({ icon, aria, action }) => + {actions.map(({ + icon, aria, action, inverted + }) => ( ( + + onPressClose()} /> + +); + +Explorer.propTypes = { + id: PropTypes.number.isRequired, + onPressClose: PropTypes.func, + canEdit: PropTypes.bool +}; +Explorer.defaultProps = { + canEdit: false, + onPressClose: () => {} +}; + +export default Explorer; diff --git a/client/components/mobile/FloatingNav.jsx b/client/components/mobile/FloatingNav.jsx new file mode 100644 index 00000000..de19c4ff --- /dev/null +++ b/client/components/mobile/FloatingNav.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { remSize, prop } from '../../theme'; +import Button from '../../common/Button'; +import IconButton from './IconButton'; + +const FloatingContainer = styled.div` + position: fixed; + right: ${remSize(16)}; + top: ${remSize(80)}; + + text-align: right; + z-index: 3; + + svg { width: ${remSize(32)}; }; + svg > path { fill: ${prop('Button.default.background')} !important }; +`; + +const FloatingNav = ({ items }) => ( + + { items.map(({ icon, onPress }) => + ( + + ))} + +); + +FloatingNav.propTypes = { + items: PropTypes.arrayOf(PropTypes.shape({ + icon: PropTypes.element, + onPress: PropTypes.func + })) +}; + +FloatingNav.defaultProps = { + items: [] +}; + +export default FloatingNav; diff --git a/client/components/mobile/Header.jsx b/client/components/mobile/Header.jsx index 28689037..993bf583 100644 --- a/client/components/mobile/Header.jsx +++ b/client/components/mobile/Header.jsx @@ -14,7 +14,7 @@ const textColor = ({ transparent, inverted }) => prop((transparent === false && const HeaderDiv = styled.div` - position: fixed; + ${props => props.fixed && 'position: fixed;'} width: 100%; background: ${props => background(props)}; color: ${textColor}; @@ -63,9 +63,9 @@ const TitleContainer = styled.div` const Header = ({ title, subtitle, leftButton, children, - transparent, inverted, slim + transparent, inverted, slim, fixed }) => ( - + {leftButton} {title &&

{title}

} @@ -86,6 +86,7 @@ Header.propTypes = { transparent: PropTypes.bool, inverted: PropTypes.bool, slim: PropTypes.bool, + fixed: PropTypes.bool, }; Header.defaultProps = { @@ -95,7 +96,8 @@ Header.defaultProps = { children: [], transparent: false, inverted: false, - slim: false + slim: false, + fixed: true }; export default Header; diff --git a/client/components/mobile/IDEWrapper.jsx b/client/components/mobile/IDEWrapper.jsx index 0982cf81..5e66ee01 100644 --- a/client/components/mobile/IDEWrapper.jsx +++ b/client/components/mobile/IDEWrapper.jsx @@ -2,7 +2,10 @@ import React from 'react'; import styled from 'styled-components'; import { remSize } from '../../theme'; +// Applies padding to top and bottom so editor content is always visible + export default styled.div` z-index: 0; margin-top: ${remSize(16)}; + .CodeMirror-sizer > * { padding-bottom: ${remSize(320)}; }; `; diff --git a/client/components/mobile/Sidebar.jsx b/client/components/mobile/Sidebar.jsx new file mode 100644 index 00000000..bc6c13ab --- /dev/null +++ b/client/components/mobile/Sidebar.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router'; +import styled from 'styled-components'; +import { remSize, prop, common } from '../../theme'; +import Header from './Header'; +import IconButton from './IconButton'; +import { ExitIcon } from '../../common/icons'; + + +const SidebarWrapper = styled.div` + height: 100%; + width: ${remSize(180)}; + + position: fixed; + z-index: 2; + left: 0; + + background: white; + box-shadow: 0 6px 6px 0 rgba(0,0,0,0.10); +`; + +const Sidebar = ({ title, onPressClose, children }) => ( + + {title && +
+ +
} + {children} +
+); + +Sidebar.propTypes = { + title: PropTypes.string, + onPressClose: PropTypes.func, + children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]), +}; + +Sidebar.defaultProps = { + title: null, + children: [], + onPressClose: () => {} +}; + + +export default Sidebar; diff --git a/client/components/useAsModal.jsx b/client/components/useAsModal.jsx index 1afa0d30..350d1de2 100644 --- a/client/components/useAsModal.jsx +++ b/client/components/useAsModal.jsx @@ -1,10 +1,29 @@ import React from 'react'; +import styled from 'styled-components'; import { useModalBehavior } from '../utils/custom-hooks'; -export default (component) => { - const [visible, trigger, setRef] = useModalBehavior(); +const BackgroundOverlay = styled.div` + position: fixed; + z-index: 2; + width: 100% !important; + height: 100% !important; + + background: black; + opacity: 0.3; +`; - const wrapper = () =>
{visible && component}
; // eslint-disable-line +export default (Element, hasOverlay = false) => { + const [visible, toggle, setRef] = useModalBehavior(); - return [trigger, wrapper]; + const wrapper = () => (visible && +
+ {hasOverlay && } +
+ { (typeof (Element) === 'function') + ? Element(toggle) + : Element} +
+
); + + return [toggle, wrapper]; }; diff --git a/client/images/circle-folder.svg b/client/images/circle-folder.svg new file mode 100644 index 00000000..ab2076b9 --- /dev/null +++ b/client/images/circle-folder.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/images/circle-info.svg b/client/images/circle-info.svg new file mode 100644 index 00000000..ed75766b --- /dev/null +++ b/client/images/circle-info.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/images/circle-terminal.svg b/client/images/circle-terminal.svg new file mode 100644 index 00000000..168efd85 --- /dev/null +++ b/client/images/circle-terminal.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/images/folder-padded.svg b/client/images/folder-padded.svg new file mode 100644 index 00000000..67980f0a --- /dev/null +++ b/client/images/folder-padded.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx index 61fed8ce..3b74e1fc 100644 --- a/client/modules/App/App.jsx +++ b/client/modules/App/App.jsx @@ -19,7 +19,9 @@ class App extends React.Component { componentWillReceiveProps(nextProps) { const locationWillChange = nextProps.location !== this.props.location; - const shouldSkipRemembering = nextProps.location.state && nextProps.location.state.skipSavingPath === true; + const shouldSkipRemembering = + nextProps.location.state && + nextProps.location.state.skipSavingPath === true; if (locationWillChange && !shouldSkipRemembering) { this.props.setPreviousPath(this.props.location.pathname); diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index e7869f26..d0a10ef8 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -108,10 +108,15 @@ export class FileNode extends React.Component { handleFileClick = (event) => { event.stopPropagation(); const { isDeleting } = this.state; - const { id, setSelectedFile, name } = this.props; + const { + id, setSelectedFile, name, onClickFile + } = this.props; if (name !== 'root' && !isDeleting) { setSelectedFile(id); } + + // debugger; // eslint-disable-line + if (onClickFile) { onClickFile(); } } handleFileNameChange = (event) => { @@ -214,7 +219,7 @@ export class FileNode extends React.Component { renderChild = childId => (
  • - +
  • ) @@ -233,7 +238,7 @@ export class FileNode extends React.Component { const isRoot = this.props.name === 'root'; return ( -
    +
    { !isRoot &&
    @@ -382,10 +387,12 @@ FileNode.propTypes = { hideFolderChildren: PropTypes.func.isRequired, canEdit: PropTypes.bool.isRequired, openUploadFileModal: PropTypes.func.isRequired, - authenticated: PropTypes.bool.isRequired + authenticated: PropTypes.bool.isRequired, + onClickFile: PropTypes.func }; FileNode.defaultProps = { + onClickFile: null, parentId: '0', isSelectedFile: false, isFolderClosed: false, diff --git a/client/modules/IDE/pages/MobileIDEView.jsx b/client/modules/IDE/pages/MobileIDEView.jsx index f924db16..0841da80 100644 --- a/client/modules/IDE/pages/MobileIDEView.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -16,7 +16,8 @@ import * as PreferencesActions from '../actions/preferences'; // Local Imports import Editor from '../components/Editor'; -import { PlayIcon, MoreIcon } from '../../../common/icons'; + +import { PlayIcon, MoreIcon, FolderIcon, PreferencesIcon, TerminalIcon, SaveIcon } from '../../../common/icons'; import UnsavedChangesDotIcon from '../../../images/unsaved-changes-dot.svg'; import IconButton from '../../../components/mobile/IconButton'; @@ -24,12 +25,12 @@ import Header from '../../../components/mobile/Header'; import Screen from '../../../components/mobile/MobileScreen'; import Footer from '../../../components/mobile/Footer'; import IDEWrapper from '../../../components/mobile/IDEWrapper'; +import MobileExplorer from '../../../components/mobile/Explorer'; import Console from '../components/Console'; import { remSize } from '../../../theme'; -// import OverlayManager from '../../../components/OverlayManager'; + import ActionStrip from '../../../components/mobile/ActionStrip'; import useAsModal from '../../../components/useAsModal'; -import { PreferencesIcon, TerminalIcon, SaveIcon } from '../../../common/icons'; import Dropdown from '../../../components/Dropdown'; @@ -46,6 +47,8 @@ const withChangeDot = (title, unsavedChanges = false) => ( ); +const getRootFile = files => files && files.filter(file => file.name === 'root')[0]; +const getRootFileID = files => (root => root && root.id)(getRootFile(files)); const Expander = styled.div` height: ${props => (props.expanded ? remSize(160) : remSize(27))}; @@ -55,7 +58,7 @@ const NavItem = styled.li` position: relative; `; -const getNatOptions = (username = undefined) => +const getNavOptions = (username = undefined) => (username ? [ { icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', }, @@ -170,7 +173,7 @@ const autosave = (autosaveInterval, setAutosaveInterval) => (props, prevProps) = const MobileIDEView = (props) => { const { ide, preferences, project, selectedFile, user, params, unsavedChanges, expandConsole, collapseConsole, - stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject + stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject, files } = props; @@ -180,10 +183,6 @@ const MobileIDEView = (props) => { const { consoleIsExpanded } = ide; const { name: filename } = selectedFile; - const [triggerNavDropdown, NavDropDown] = useAsModal(); // Force state reset useEffect(clearPersistedState, []); @@ -204,6 +203,18 @@ const MobileIDEView = (props) => { setCurrentProjectID(params.project_id); }, [params, project, username]); + // Screen Modals + const [toggleNavDropdown, NavDropDown] = useAsModal(); + + const [toggleExplorer, Explorer] = useAsModal(toggle => + (), true); // TODO: This behavior could move to const [autosaveInterval, setAutosaveInterval] = useState(null); @@ -214,19 +225,23 @@ const MobileIDEView = (props) => { useEventListener('keydown', handleGlobalKeydown(props, cmController), false, [props]); const projectActions = - [{ icon: TerminalIcon, aria: 'Toggle console open/closed', action: consoleIsExpanded ? collapseConsole : expandConsole }, - { icon: SaveIcon, aria: 'Save project', action: () => saveProject(cmController.getContent(), false, true) } + [{ + icon: TerminalIcon, aria: 'Toggle console open/closed', action: consoleIsExpanded ? collapseConsole : expandConsole, inverted: true + }, + { icon: SaveIcon, aria: 'Save project', action: () => saveProject(cmController.getContent(), false, true) }, + { icon: FolderIcon, aria: 'Open files explorer', action: toggleExplorer } ]; return ( +
    @@ -299,6 +314,8 @@ MobileIDEView.propTypes = { username: PropTypes.string, }).isRequired, + getProject: PropTypes.func.isRequired, + clearPersistedState: PropTypes.func.isRequired, params: PropTypes.shape({ project_id: PropTypes.string, username: PropTypes.string @@ -308,8 +325,6 @@ MobileIDEView.propTypes = { startSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, - getProject: PropTypes.func.isRequired, - clearPersistedState: PropTypes.func.isRequired, autosaveProject: PropTypes.func.isRequired, diff --git a/client/utils/custom-hooks.js b/client/utils/custom-hooks.js index b0981ae0..9271287e 100644 --- a/client/utils/custom-hooks.js +++ b/client/utils/custom-hooks.js @@ -28,7 +28,7 @@ export const useModalBehavior = (hideOverlay) => { const handleClickOutside = ({ target }) => { - if (ref && ref.current && !ref.current.contains(target)) { + if (ref && ref.current && !(ref.current.contains && ref.current.contains(target))) { hide(); } };