diff --git a/client/components/mobile/Header.jsx b/client/components/mobile/Header.jsx index eca2c98e..1f7f7a29 100644 --- a/client/components/mobile/Header.jsx +++ b/client/components/mobile/Header.jsx @@ -1,14 +1,16 @@ import React from 'react'; import styled from 'styled-components'; +import PropTypes from 'prop-types'; import { prop, remSize } from '../../theme'; const background = prop('MobilePanel.default.background'); const textColor = prop('primaryTextColor'); -const Header = styled.div` + +const HeaderDiv = styled.div` position: fixed; width: 100%; - background: ${background}; + background: ${props => (props.transparent ? 'transparent' : background)}; color: ${textColor}; padding: ${remSize(12)}; padding-left: ${remSize(16)}; @@ -23,8 +25,56 @@ const Header = styled.div` // TODO: svg { - height: 2rem; + max-height: ${remSize(32)}; + padding: ${remSize(4)} } `; +const IconContainer = styled.div` + margin-left: ${props => (props.leftButton ? remSize(32) : remSize(4))}; + display: flex; +`; + + +const TitleContainer = styled.div` + margin-left: ${remSize(4)}; + margin-right: auto; + + ${props => props.padded && `h2{ + padding-top: ${remSize(10)}; + padding-bottom: ${remSize(10)}; + }`} +`; + +const Header = ({ + title, subtitle, leftButton, children, transparent +}) => ( + + {leftButton} + + {title &&

{title}

} + {subtitle &&

{subtitle}

} +
+ + {children} + +
+); + +Header.propTypes = { + title: PropTypes.string, + subtitle: PropTypes.string, + leftButton: PropTypes.element, + children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]), + transparent: PropTypes.bool +}; + +Header.defaultProps = { + title: null, + subtitle: null, + leftButton: null, + children: [], + transparent: false +}; + export default Header; diff --git a/client/components/mobile/IconButton.jsx b/client/components/mobile/IconButton.jsx index 248dd014..08f05311 100644 --- a/client/components/mobile/IconButton.jsx +++ b/client/components/mobile/IconButton.jsx @@ -2,9 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import Button from '../../common/Button'; +import { remSize } from '../../theme'; const ButtonWrapper = styled(Button)` -width: 3rem; +width: ${remSize(48)}; > svg { width: 100%; height: 100%; diff --git a/client/components/mobile/MobileScreen.jsx b/client/components/mobile/MobileScreen.jsx index 1e50f80a..e78baa2c 100644 --- a/client/components/mobile/MobileScreen.jsx +++ b/client/components/mobile/MobileScreen.jsx @@ -1,13 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; -const Screen = ({ children }) => ( -
+const Screen = ({ children, fullscreen }) => ( +
{children}
); + +Screen.defaultProps = { + fullscreen: false +}; + Screen.propTypes = { - children: PropTypes.node.isRequired + children: PropTypes.node.isRequired, + fullscreen: PropTypes.bool }; export default Screen; diff --git a/client/components/mobile/PreferencePicker.jsx b/client/components/mobile/PreferencePicker.jsx new file mode 100644 index 00000000..0e2e085a --- /dev/null +++ b/client/components/mobile/PreferencePicker.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { prop, remSize } from '../../theme'; + + +const PreferenceTitle = styled.h4.attrs(props => ({ ...props, className: 'preference__title' }))` + color: ${prop('primaryTextColor')}; +`; + +const Preference = styled.div.attrs(props => ({ ...props, className: 'preference' }))` + flex-wrap: nowrap !important; + align-items: baseline !important; + justify-items: space-between; +`; + +const OptionLabel = styled.label.attrs({ className: 'preference__option' })` + font-size: ${remSize(14)} !important; +`; + +const PreferencePicker = ({ + title, value, onSelect, options, +}) => ( + + {title} +
+ {options.map(option => ( + + onSelect(option.value)} + aria-label={option.ariaLabel} + name={option.name} + key={`${option.name}-${option.id}-input`} + id={option.id} + className="preference__radio-button" + value={option.value} + checked={value === option.value} + /> + + {option.label} + + ))} +
+
+); + +PreferencePicker.defaultProps = { + options: [] +}; + +PreferencePicker.propTypes = { + title: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired, + options: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + label: PropTypes.string, + ariaLabel: PropTypes.string, + })), + onSelect: PropTypes.func.isRequired, +}; + +export default PreferencePicker; diff --git a/client/modules/IDE/pages/IDEViewMobile.jsx b/client/modules/IDE/pages/MobileIDEView.jsx similarity index 89% rename from client/modules/IDE/pages/IDEViewMobile.jsx rename to client/modules/IDE/pages/MobileIDEView.jsx index 9a638d60..180cbb2e 100644 --- a/client/modules/IDE/pages/IDEViewMobile.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -1,7 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import styled from 'styled-components'; -import { Link } from 'react-router'; import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import { useState } from 'react'; @@ -27,16 +25,10 @@ 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 { remSize } from '../../../theme'; - -const IconContainer = styled.div` - margin-left: ${remSize(32)}; - display: flex; -`; const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id); -const IDEViewMobile = (props) => { +const MobileIDEView = (props) => { const { preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage, selectedFile, updateFileContent, files, @@ -49,22 +41,21 @@ const IDEViewMobile = (props) => { const [overlay, setOverlay] = useState(null); // eslint-disable-line return ( - -
- -
-

{project.name}

-

{selectedFile.name}

-
- - - setOverlay('preferences')} - icon={PreferencesIcon} - aria-label="Open preferences menu" - /> - { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" /> - + +
+ } + > + setOverlay('preferences')} + icon={PreferencesIcon} + aria-label="Open preferences menu" + /> + { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" />
@@ -109,7 +100,7 @@ const IDEViewMobile = (props) => { }; -IDEViewMobile.propTypes = { +MobileIDEView.propTypes = { preferences: PropTypes.shape({ fontSize: PropTypes.number.isRequired, @@ -256,4 +247,4 @@ function mapDispatchToProps(dispatch) { } -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEViewMobile)); +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView)); diff --git a/client/modules/Mobile/MobilePreferences.jsx b/client/modules/Mobile/MobilePreferences.jsx new file mode 100644 index 00000000..a9dc9f34 --- /dev/null +++ b/client/modules/Mobile/MobilePreferences.jsx @@ -0,0 +1,226 @@ +import React from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +import * as PreferencesActions from '../IDE/actions/preferences'; +import * as IdeActions from '../IDE/actions/ide'; + +import IconButton from '../../components/mobile/IconButton'; +import Screen from '../../components/mobile/MobileScreen'; +import Header from '../../components/mobile/Header'; +import PreferencePicker from '../../components/mobile/PreferencePicker'; +import { ExitIcon } from '../../common/icons'; +import { remSize, prop } from '../../theme'; + +const Content = styled.div` + z-index: 0; + margin-top: ${remSize(68)}; +`; + + +const SettingsHeader = styled(Header)` + background: transparent; +`; + +const SectionHeader = styled.h2` + color: ${prop('primaryTextColor')}; + padding-top: ${remSize(32)}; +`; + +const SectionSubeader = styled.h3` + color: ${prop('primaryTextColor')}; +`; + + +const MobilePreferences = (props) => { + const { + setTheme, setAutosave, setLinewrap, setTextOutput, setGridOutput, setSoundOutput, lineNumbers, lintWarning + } = props; + const { + theme, autosave, linewrap, textOutput, gridOutput, soundOutput, setLineNumbers, setLintWarning + } = props; + + const generalSettings = [ + { + title: 'Theme', + value: theme, + options: [ + { + value: 'light', label: 'light', ariaLabel: 'light theme on', name: 'light theme', id: 'light-theme-on' + }, + { + value: 'dark', label: 'dark', ariaLabel: 'dark theme on', name: 'dark theme', id: 'dark-theme-on' + }, + { + value: 'contrast', + label: 'contrast', + ariaLabel: 'contrast theme on', + name: 'contrast theme', + id: 'contrast-theme-on' + } + ], + onSelect: x => setTheme(x) // setTheme + }, + + { + title: 'Autosave', + value: autosave, + options: [ + { + value: true, label: 'On', ariaLabel: 'autosave on', name: 'autosave', id: 'autosave-on' + }, + { + value: false, label: 'Off', ariaLabel: 'autosave off', name: 'autosave', id: 'autosave-off' + }, + ], + onSelect: x => setAutosave(x) // setAutosave + }, + + { + title: 'Word Wrap', + value: linewrap, + options: [ + { + value: true, label: 'On', ariaLabel: 'linewrap on', name: 'linewrap', id: 'linewrap-on' + }, + { + value: false, label: 'Off', ariaLabel: 'linewrap off', name: 'linewrap', id: 'linewrap-off' + }, + ], + onSelect: x => setLinewrap(x) + } + ]; + + const outputSettings = [ + { + title: 'Plain-text', + value: textOutput, + options: [ + { + value: true, label: 'On', ariaLabel: 'text output on', name: 'text output', id: 'text-output-on' + }, + { + value: false, label: 'Off', ariaLabel: 'text output off', name: 'text output', id: 'text-output-off' + }, + ], + onSelect: x => setTextOutput(x) + }, + { + title: 'Table-text', + value: gridOutput, + options: [ + { + value: true, label: 'On', ariaLabel: 'table output on', name: 'table output', id: 'table-output-on' + }, + { + value: false, label: 'Off', ariaLabel: 'table output off', name: 'table output', id: 'table-output-off' + }, + ], + onSelect: x => setGridOutput(x) + }, + { + title: 'Sound', + value: soundOutput, + options: [ + { + value: true, label: 'On', ariaLabel: 'sound output on', name: 'sound output', id: 'sound-output-on' + }, + { + value: false, label: 'Off', ariaLabel: 'sound output off', name: 'sound output', id: 'sound-output-off' + }, + ], + onSelect: x => setSoundOutput(x) + }, + ]; + + const accessibilitySettings = [ + { + title: 'Line Numbers', + value: lineNumbers, + options: [ + { + value: true, label: 'On', ariaLabel: 'line numbers on', name: 'line numbers', id: 'line-numbers-on' + }, + { + value: false, label: 'Off', ariaLabel: 'line numbers off', name: 'line numbers', id: 'line-numbers-off' + }, + ], + onSelect: x => setLineNumbers(x) + }, + { + title: 'Lint Warning Sound', + value: lintWarning, + options: [ + { + value: true, label: 'On', ariaLabel: 'lint warning on', name: 'lint warning', id: 'lint-warning-on' + }, + { + value: false, label: 'Off', ariaLabel: 'lint warning off', name: 'lint warning', id: 'lint-warning-off' + }, + ], + onSelect: x => setLintWarning(x) + }, + ]; + + return ( + +
+ + + + +
+ + General Settings + { generalSettings.map(option => ) } + + Accessibility + { accessibilitySettings.map(option => ) } + + Accessible Output + Used with screen reader + { outputSettings.map(option => ) } + + +
+
+
); +}; + + +MobilePreferences.propTypes = { + fontSize: PropTypes.number.isRequired, + lineNumbers: PropTypes.bool.isRequired, + autosave: PropTypes.bool.isRequired, + linewrap: PropTypes.bool.isRequired, + textOutput: PropTypes.bool.isRequired, + gridOutput: PropTypes.bool.isRequired, + soundOutput: PropTypes.bool.isRequired, + lintWarning: PropTypes.bool.isRequired, + theme: PropTypes.string.isRequired, + + setLinewrap: PropTypes.func.isRequired, + setLintWarning: PropTypes.func.isRequired, + setTheme: PropTypes.func.isRequired, + setFontSize: PropTypes.func.isRequired, + setLineNumbers: PropTypes.func.isRequired, + setAutosave: PropTypes.func.isRequired, + setTextOutput: PropTypes.func.isRequired, + setGridOutput: PropTypes.func.isRequired, + setSoundOutput: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => ({ + ...state.preferences, +}); + +const mapDispatchToProps = dispatch => bindActionCreators({ + ...PreferencesActions, + ...IdeActions +}, dispatch); + + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobilePreferences)); diff --git a/client/modules/Mobile/MobileSketchView.jsx b/client/modules/Mobile/MobileSketchView.jsx index 37dc5ba9..64eabb5e 100644 --- a/client/modules/Mobile/MobileSketchView.jsx +++ b/client/modules/Mobile/MobileSketchView.jsx @@ -1,6 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Link } from 'react-router'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import styled from 'styled-components'; @@ -48,14 +47,13 @@ const MobileSketchView = (props) => { const { preferences, ide } = props; return ( - -
- -
-

{projectName}

-


-
-
+ +
+ } + title={projectName} + /> ( - + + ); diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index f7666f29..c038a3c7 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -123,7 +123,7 @@ if (process.env.MOBILE_ENABLED) { res.send(renderIndex()); }); - router.get('/mobile/*', (req, res) => { + router.get('/mobile/preferences', (req, res) => { res.send(renderIndex()); }); }