From 968a9c65afc6707ddf36bcac7caf0dcb7b5beddc Mon Sep 17 00:00:00 2001 From: Steven Hao Date: Mon, 24 Mar 2025 00:31:24 -0700 Subject: [PATCH 1/3] wip --- src/components/Chat/Chat.js | 39 +++++++++- src/components/Chat/css/index.css | 51 ++++++++++++ src/components/Player/MobileGridControls.js | 78 ++++++++++++++++++- src/components/Player/css/index.css | 11 ++- .../Player/css/mobileGridControls.css | 64 +++++++++++++++ src/components/Player/css/mobileKeyboard.css | 47 +++++++++++ src/components/Toolbar/css/index.css | 30 +++++++ src/components/Toolbar/index.js | 9 +-- src/index.js | 1 + src/lib/jsUtils.js | 4 + 10 files changed, 322 insertions(+), 12 deletions(-) diff --git a/src/components/Chat/Chat.js b/src/components/Chat/Chat.js index 131c52d1..592e5626 100644 --- a/src/components/Chat/Chat.js +++ b/src/components/Chat/Chat.js @@ -164,6 +164,38 @@ export default class Chat extends Component { ); } + + renderMobileChat() { + return ( +
+
+
Chat
+
+ +
+
+
+ {this.renderChatHeader()} + {this.renderChatSubheader()} +
{ + if (el) { + el.scrollTop = el.scrollHeight; + } + }} + className="chat--messages" + > + {this.props.data.messages && this.props.data.messages.map((message, i) => ( +
{this.renderMessage(message)}
+ ))} +
+
+
+ {this.renderChatBar()} +
+
+ ); + } renderFencingOptions() { const fencingUrl = `/fencing/${this.props.gid}`; @@ -411,10 +443,15 @@ export default class Chat extends Component { render() { const messages = this.mergeMessages(this.props.data, this.props.opponentData); + + if (this.props.mobile && this.props.chatOpen) { + return this.renderMobileChat(); + } + return ( {this.renderToolbar()} -
+
{this.renderChatHeader()} {this.renderChatSubheader()}
{ @@ -525,14 +529,84 @@ export default class MobileGridControls extends GridControls { ); } + toggleClueList = () => { + this.setState(state => ({ showClueList: !state.showClueList })); + }; + + handleSelectClue = (direction, number) => { + this.props.onSetDirection(direction); + const grid = this.grid; + const clueRoot = grid.getCellByNumber(number); + if (clueRoot) { + this.props.onSetSelected(clueRoot); + } + }; + + renderOnscreenKeyboard() { + if (!this.state.showKeyboard) return null; + + return ( +
+ {'QWERTYUIOP'.split('').map(letter => ( + + ))} + {'ASDFGHJKL'.split('').map(letter => ( + + ))} + {'ZXCVBNM'.split('').map(letter => ( + + ))} + + +
+ ); + } + + handleKeyClick = (letter) => { + this.typeLetter(letter, false, { nextClueIfFilled: true }); + }; + + handleBackspaceClick = () => { + this.backspace(); + }; + + handleToggleDirection = () => { + this.flipDirection(); + }; + render() { + const { showClueList } = this.state; + return ( // eslint-disable-next-line react/no-string-refs
{this.renderClueBar()} - {this.renderGridContent()} +
+ {this.renderGridContent()} +
{this.renderMobileInputs()} - {/* {this.renderMobileKeyboard()} */} + {this.renderOnscreenKeyboard()} + {showClueList && ( + + )} {this.props.enableDebug && (this.state.dbgstr || 'No message')}
); diff --git a/src/components/Player/css/index.css b/src/components/Player/css/index.css index 7a0870dd..f7439711 100644 --- a/src/components/Player/css/index.css +++ b/src/components/Player/css/index.css @@ -91,18 +91,27 @@ .player--mobile--wrapper { display: flex; flex: 1; + flex-direction: column; + margin-top: 48px; /* Account for fixed toolbar */ + height: calc(100vh - 48px); + overflow: hidden; } .player--mobile { flex: 1; display: flex; flex-direction: column; - justify-content: center; + justify-content: flex-start; align-items: center; + overflow-y: auto; + padding-bottom: 200px; /* Space for keyboard */ } .player--mobile--grid { display: flex; + width: 100%; + max-width: 600px; + margin: 0 auto; } .player--mobile--list-view { diff --git a/src/components/Player/css/mobileGridControls.css b/src/components/Player/css/mobileGridControls.css index 993d004f..45484986 100644 --- a/src/components/Player/css/mobileGridControls.css +++ b/src/components/Player/css/mobileGridControls.css @@ -84,3 +84,67 @@ display: flex; align-items: center; } + +/* Clue list overlay */ +.mobile-grid-controls--clue-list-overlay { + position: fixed; + top: 48px; /* below toolbar */ + left: 0; + width: 100%; + height: calc(100% - 48px); + background: white; + z-index: 10; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.mobile-grid-controls--clue-list-header { + display: flex; + justify-content: space-between; + padding: 10px; + border-bottom: 1px solid #eee; + background: var(--main-blue-3); +} + +.mobile-grid-controls--clue-list-close { + font-size: 24px; + cursor: pointer; +} + +.mobile-grid-controls--clue-list-content { + flex: 1; + overflow-y: auto; + padding: 0 10px; +} + +.mobile-grid-controls--clue-section { + margin: 10px 0; +} + +.mobile-grid-controls--clue-section-header { + font-weight: bold; + margin-bottom: 5px; +} + +.mobile-grid-controls--clue-item { + padding: 8px 5px; + border-bottom: 1px solid #eee; + display: flex; +} + +.mobile-grid-controls--clue-item.active { + background-color: var(--main-blue-3); +} + +.mobile-grid-controls--clue-item-number { + font-weight: bold; + margin-right: 10px; + min-width: 30px; +} + +/* Grid wrapper for scrolling above keyboard */ +.grid-wrapper { + padding-bottom: 200px; /* space for keyboard */ + overflow-y: auto; +} diff --git a/src/components/Player/css/mobileKeyboard.css b/src/components/Player/css/mobileKeyboard.css index a59ccb78..8e17ea95 100644 --- a/src/components/Player/css/mobileKeyboard.css +++ b/src/components/Player/css/mobileKeyboard.css @@ -50,3 +50,50 @@ width: 40px; letter-spacing: 2px; } + +/* Onscreen keyboard styles */ +.onscreen-keyboard { + display: flex; + flex-wrap: wrap; + padding: 5px; + background: #ddd; + border-top: 1px solid #bbb; + position: fixed; + bottom: 0; + left: 0; + width: 100%; + box-sizing: border-box; + z-index: 5; +} + +.key-btn { + flex: 0 0 10%; /* roughly 10 buttons per row */ + margin: 2px; + font-size: 1.1rem; + padding: 10px 0; + border: none; + border-radius: 4px; + background: #fff; +} + +.key-btn:active { + background: #ccc; +} + +.key-btn.wide { + flex: 0 0 20%; /* wider buttons like backspace */ +} + +/* Dark mode support */ +body.dark-mode .onscreen-keyboard { + background: #555; +} + +body.dark-mode .key-btn { + background: #444; + color: #fff; +} + +body.dark-mode .key-btn:active { + background: #666; +} diff --git a/src/components/Toolbar/css/index.css b/src/components/Toolbar/css/index.css index 6441d173..4c87bf15 100644 --- a/src/components/Toolbar/css/index.css +++ b/src/components/Toolbar/css/index.css @@ -112,6 +112,11 @@ code { justify-content: space-between; font-size: 14pt; color: white; + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 10; } .toolbar--mobile a:first-child { @@ -139,6 +144,7 @@ code { align-items: center; position: relative; text-align: center; + width: 100%; } .toolbar--mobile .clock { @@ -148,3 +154,27 @@ code { .toolbar--mobile .toolbar--expand:last-child { margin-right: 16px; } + +/* Mobile Menu Styles */ +.toolbar--mobile-menu { + position: fixed; + top: 48px; + left: 0; + width: 100%; + background-color: #f8f8f8; + z-index: 9; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + display: flex; + flex-direction: column; + padding: 10px 0; +} + +.toolbar--mobile-menu-item { + padding: 12px 16px; + font-size: 16px; + cursor: pointer; +} + +.toolbar--mobile-menu-item:active { + background-color: #eee; +} diff --git a/src/components/Toolbar/index.js b/src/components/Toolbar/index.js index 756d95bb..4b1b0666 100644 --- a/src/components/Toolbar/index.js +++ b/src/components/Toolbar/index.js @@ -229,18 +229,11 @@ export default class Toolbar extends Component { renderListViewButton() { const {listMode, mobile} = this.props; if (mobile) { - if (listMode) { - return ( - - ); - } return ( ); } diff --git a/src/index.js b/src/index.js index 690dc8f8..bb3ba55d 100644 --- a/src/index.js +++ b/src/index.js @@ -23,6 +23,7 @@ import GlobalContext from './lib/GlobalContext'; import './style.css'; import './dark.css'; +import './global.css'; const darkModeLocalStorageKey = 'dark_mode_preference'; diff --git a/src/lib/jsUtils.js b/src/lib/jsUtils.js index 3d39b76e..87996cf9 100644 --- a/src/lib/jsUtils.js +++ b/src/lib/jsUtils.js @@ -119,6 +119,10 @@ function isAncestor(a, b) { } function isMobile() { + if (typeof window !== 'undefined' && window.innerWidth < 600) { + return true; + } + if (navigator.userAgent.match(/Tablet|iPad/i)) { // do tablet stuff return true; From 22f113b49b942f97be2928f0ff5cbb641e9990d8 Mon Sep 17 00:00:00 2001 From: Jacob Teo Date: Mon, 24 Mar 2025 01:54:02 -0700 Subject: [PATCH 2/3] Revert "wip" This reverts commit 968a9c65afc6707ddf36bcac7caf0dcb7b5beddc. --- src/components/Chat/Chat.js | 39 +--------- src/components/Chat/css/index.css | 51 ------------ src/components/Player/MobileGridControls.js | 78 +------------------ src/components/Player/css/index.css | 11 +-- .../Player/css/mobileGridControls.css | 64 --------------- src/components/Player/css/mobileKeyboard.css | 47 ----------- src/components/Toolbar/css/index.css | 30 ------- src/components/Toolbar/index.js | 9 ++- src/index.js | 1 - src/lib/jsUtils.js | 4 - 10 files changed, 12 insertions(+), 322 deletions(-) diff --git a/src/components/Chat/Chat.js b/src/components/Chat/Chat.js index 592e5626..131c52d1 100644 --- a/src/components/Chat/Chat.js +++ b/src/components/Chat/Chat.js @@ -164,38 +164,6 @@ export default class Chat extends Component { ); } - - renderMobileChat() { - return ( -
-
-
Chat
-
- -
-
-
- {this.renderChatHeader()} - {this.renderChatSubheader()} -
{ - if (el) { - el.scrollTop = el.scrollHeight; - } - }} - className="chat--messages" - > - {this.props.data.messages && this.props.data.messages.map((message, i) => ( -
{this.renderMessage(message)}
- ))} -
-
-
- {this.renderChatBar()} -
-
- ); - } renderFencingOptions() { const fencingUrl = `/fencing/${this.props.gid}`; @@ -443,15 +411,10 @@ export default class Chat extends Component { render() { const messages = this.mergeMessages(this.props.data, this.props.opponentData); - - if (this.props.mobile && this.props.chatOpen) { - return this.renderMobileChat(); - } - return ( {this.renderToolbar()} -
+
{this.renderChatHeader()} {this.renderChatSubheader()}
{ @@ -529,84 +525,14 @@ export default class MobileGridControls extends GridControls { ); } - toggleClueList = () => { - this.setState(state => ({ showClueList: !state.showClueList })); - }; - - handleSelectClue = (direction, number) => { - this.props.onSetDirection(direction); - const grid = this.grid; - const clueRoot = grid.getCellByNumber(number); - if (clueRoot) { - this.props.onSetSelected(clueRoot); - } - }; - - renderOnscreenKeyboard() { - if (!this.state.showKeyboard) return null; - - return ( -
- {'QWERTYUIOP'.split('').map(letter => ( - - ))} - {'ASDFGHJKL'.split('').map(letter => ( - - ))} - {'ZXCVBNM'.split('').map(letter => ( - - ))} - - -
- ); - } - - handleKeyClick = (letter) => { - this.typeLetter(letter, false, { nextClueIfFilled: true }); - }; - - handleBackspaceClick = () => { - this.backspace(); - }; - - handleToggleDirection = () => { - this.flipDirection(); - }; - render() { - const { showClueList } = this.state; - return ( // eslint-disable-next-line react/no-string-refs
{this.renderClueBar()} -
- {this.renderGridContent()} -
+ {this.renderGridContent()} {this.renderMobileInputs()} - {this.renderOnscreenKeyboard()} - {showClueList && ( - - )} + {/* {this.renderMobileKeyboard()} */} {this.props.enableDebug && (this.state.dbgstr || 'No message')}
); diff --git a/src/components/Player/css/index.css b/src/components/Player/css/index.css index f7439711..7a0870dd 100644 --- a/src/components/Player/css/index.css +++ b/src/components/Player/css/index.css @@ -91,27 +91,18 @@ .player--mobile--wrapper { display: flex; flex: 1; - flex-direction: column; - margin-top: 48px; /* Account for fixed toolbar */ - height: calc(100vh - 48px); - overflow: hidden; } .player--mobile { flex: 1; display: flex; flex-direction: column; - justify-content: flex-start; + justify-content: center; align-items: center; - overflow-y: auto; - padding-bottom: 200px; /* Space for keyboard */ } .player--mobile--grid { display: flex; - width: 100%; - max-width: 600px; - margin: 0 auto; } .player--mobile--list-view { diff --git a/src/components/Player/css/mobileGridControls.css b/src/components/Player/css/mobileGridControls.css index 45484986..993d004f 100644 --- a/src/components/Player/css/mobileGridControls.css +++ b/src/components/Player/css/mobileGridControls.css @@ -84,67 +84,3 @@ display: flex; align-items: center; } - -/* Clue list overlay */ -.mobile-grid-controls--clue-list-overlay { - position: fixed; - top: 48px; /* below toolbar */ - left: 0; - width: 100%; - height: calc(100% - 48px); - background: white; - z-index: 10; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.mobile-grid-controls--clue-list-header { - display: flex; - justify-content: space-between; - padding: 10px; - border-bottom: 1px solid #eee; - background: var(--main-blue-3); -} - -.mobile-grid-controls--clue-list-close { - font-size: 24px; - cursor: pointer; -} - -.mobile-grid-controls--clue-list-content { - flex: 1; - overflow-y: auto; - padding: 0 10px; -} - -.mobile-grid-controls--clue-section { - margin: 10px 0; -} - -.mobile-grid-controls--clue-section-header { - font-weight: bold; - margin-bottom: 5px; -} - -.mobile-grid-controls--clue-item { - padding: 8px 5px; - border-bottom: 1px solid #eee; - display: flex; -} - -.mobile-grid-controls--clue-item.active { - background-color: var(--main-blue-3); -} - -.mobile-grid-controls--clue-item-number { - font-weight: bold; - margin-right: 10px; - min-width: 30px; -} - -/* Grid wrapper for scrolling above keyboard */ -.grid-wrapper { - padding-bottom: 200px; /* space for keyboard */ - overflow-y: auto; -} diff --git a/src/components/Player/css/mobileKeyboard.css b/src/components/Player/css/mobileKeyboard.css index 8e17ea95..a59ccb78 100644 --- a/src/components/Player/css/mobileKeyboard.css +++ b/src/components/Player/css/mobileKeyboard.css @@ -50,50 +50,3 @@ width: 40px; letter-spacing: 2px; } - -/* Onscreen keyboard styles */ -.onscreen-keyboard { - display: flex; - flex-wrap: wrap; - padding: 5px; - background: #ddd; - border-top: 1px solid #bbb; - position: fixed; - bottom: 0; - left: 0; - width: 100%; - box-sizing: border-box; - z-index: 5; -} - -.key-btn { - flex: 0 0 10%; /* roughly 10 buttons per row */ - margin: 2px; - font-size: 1.1rem; - padding: 10px 0; - border: none; - border-radius: 4px; - background: #fff; -} - -.key-btn:active { - background: #ccc; -} - -.key-btn.wide { - flex: 0 0 20%; /* wider buttons like backspace */ -} - -/* Dark mode support */ -body.dark-mode .onscreen-keyboard { - background: #555; -} - -body.dark-mode .key-btn { - background: #444; - color: #fff; -} - -body.dark-mode .key-btn:active { - background: #666; -} diff --git a/src/components/Toolbar/css/index.css b/src/components/Toolbar/css/index.css index 4c87bf15..6441d173 100644 --- a/src/components/Toolbar/css/index.css +++ b/src/components/Toolbar/css/index.css @@ -112,11 +112,6 @@ code { justify-content: space-between; font-size: 14pt; color: white; - position: fixed; - top: 0; - left: 0; - width: 100%; - z-index: 10; } .toolbar--mobile a:first-child { @@ -144,7 +139,6 @@ code { align-items: center; position: relative; text-align: center; - width: 100%; } .toolbar--mobile .clock { @@ -154,27 +148,3 @@ code { .toolbar--mobile .toolbar--expand:last-child { margin-right: 16px; } - -/* Mobile Menu Styles */ -.toolbar--mobile-menu { - position: fixed; - top: 48px; - left: 0; - width: 100%; - background-color: #f8f8f8; - z-index: 9; - box-shadow: 0 2px 5px rgba(0,0,0,0.2); - display: flex; - flex-direction: column; - padding: 10px 0; -} - -.toolbar--mobile-menu-item { - padding: 12px 16px; - font-size: 16px; - cursor: pointer; -} - -.toolbar--mobile-menu-item:active { - background-color: #eee; -} diff --git a/src/components/Toolbar/index.js b/src/components/Toolbar/index.js index 4b1b0666..756d95bb 100644 --- a/src/components/Toolbar/index.js +++ b/src/components/Toolbar/index.js @@ -229,11 +229,18 @@ export default class Toolbar extends Component { renderListViewButton() { const {listMode, mobile} = this.props; if (mobile) { + if (listMode) { + return ( + + ); + } return ( ); } diff --git a/src/index.js b/src/index.js index bb3ba55d..690dc8f8 100644 --- a/src/index.js +++ b/src/index.js @@ -23,7 +23,6 @@ import GlobalContext from './lib/GlobalContext'; import './style.css'; import './dark.css'; -import './global.css'; const darkModeLocalStorageKey = 'dark_mode_preference'; diff --git a/src/lib/jsUtils.js b/src/lib/jsUtils.js index 87996cf9..3d39b76e 100644 --- a/src/lib/jsUtils.js +++ b/src/lib/jsUtils.js @@ -119,10 +119,6 @@ function isAncestor(a, b) { } function isMobile() { - if (typeof window !== 'undefined' && window.innerWidth < 600) { - return true; - } - if (navigator.userAgent.match(/Tablet|iPad/i)) { // do tablet stuff return true; From 45f60af8240355942ac340cde6c05d685e0f49e0 Mon Sep 17 00:00:00 2001 From: Jacob Teo Date: Mon, 24 Mar 2025 01:55:19 -0700 Subject: [PATCH 3/3] wip fixed --- src/components/Chat/Chat.js | 36 ++++++++- src/components/Chat/css/index.css | 51 +++++++++++++ src/components/Player/ClueList.js | 63 +++++++++++++++ src/components/Player/MobileGridControls.js | 76 ++++++++++++++++++- src/components/Player/css/clueList.css | 13 ++++ src/components/Player/css/index.css | 11 ++- .../Player/css/mobileGridControls.css | 64 ++++++++++++++++ src/components/Player/css/mobileKeyboard.css | 47 ++++++++++++ src/components/Toolbar/css/index.css | 30 ++++++++ src/components/Toolbar/index.js | 9 +-- src/global.css | 38 ++++++++++ src/index.js | 1 + src/lib/jsUtils.js | 8 +- 13 files changed, 433 insertions(+), 14 deletions(-) create mode 100644 src/components/Player/ClueList.js create mode 100644 src/components/Player/css/clueList.css create mode 100644 src/global.css diff --git a/src/components/Chat/Chat.js b/src/components/Chat/Chat.js index 131c52d1..c4722d77 100644 --- a/src/components/Chat/Chat.js +++ b/src/components/Chat/Chat.js @@ -165,6 +165,35 @@ export default class Chat extends Component { ); } + renderMobileChat() { + return ( +
+
+
Chat
+
+ +
+
+
+ {this.renderChatHeader()} + {this.renderChatSubheader()} +
{ + if (el) { + el.scrollTop = el.scrollHeight; + } + }} + className="chat--messages" + > + {this.props.data.messages && + this.props.data.messages.map((message, i) =>
{this.renderMessage(message)}
)} +
+
+
{this.renderChatBar()}
+
+ ); + } + renderFencingOptions() { const fencingUrl = `/fencing/${this.props.gid}`; const normalUrl = `/beta/game/${this.props.gid}`; @@ -411,10 +440,15 @@ export default class Chat extends Component { render() { const messages = this.mergeMessages(this.props.data, this.props.opponentData); + + if (this.props.mobile && this.props.chatOpen) { + return this.renderMobileChat(); + } + return ( {this.renderToolbar()} -
+
{this.renderChatHeader()} {this.renderChatSubheader()}
{ + this.props.onSelectClue(direction, number); + if (this.props.onClose) { + this.props.onClose(); + } + }; + + renderClueSection(direction, clues = {}, clueLengths) { + const title = direction === 'across' ? 'Across' : 'Down'; + const {selected} = this.props; + const isSelectedDirection = selected.direction === direction; + + return ( +
+
{title}
+ {Object.keys(clues).map((number) => { + const isSelected = isSelectedDirection && selected.number === number; + const clueText = clues[number]; + const clueLength = clueLengths && clueLengths[direction] && clueLengths[direction][number]; + const lengthText = clueLength ? ` (${clueLength})` : ''; + + return ( +
this.handleClueClick(direction, number)} + > +
{number}
+
+ +
+
+ ); + })} +
+ ); + } + + render() { + const {clues, clueLengths, onClose} = this.props; + + return ( +
+
+
Clues
+
+ +
+
+
+ {this.renderClueSection('across', clues.across, clueLengths)} + {this.renderClueSection('down', clues.down, clueLengths)} +
+
+ ); + } +} diff --git a/src/components/Player/MobileGridControls.js b/src/components/Player/MobileGridControls.js index 054f9b22..c5b8a000 100644 --- a/src/components/Player/MobileGridControls.js +++ b/src/components/Player/MobileGridControls.js @@ -7,6 +7,7 @@ import {MdKeyboardArrowLeft, MdKeyboardArrowRight} from 'react-icons/md'; import classnames from 'classnames'; import _ from 'lodash'; import Clue from './ClueText'; +import ClueList from './ClueList'; import GridControls from './GridControls'; import GridObject from '../../lib/wrappers/GridWrapper'; import * as gameUtils from '../../lib/gameUtils'; @@ -21,6 +22,8 @@ export default class MobileGridControls extends GridControls { anchors: [], transform: {scale: 1, translateX: 0, translateY: 0}, dbgstr: undefined, + showClueList: false, + showKeyboard: true, }; this.prvInput = ''; this.inputRef = React.createRef(); @@ -407,6 +410,7 @@ export default class MobileGridControls extends GridControls { focusKeyboard() { this.inputRef.current.selectionStart = this.inputRef.current.selectionEnd = this.inputRef.current.value.length; this.inputRef.current.focus(); + this.setState({showKeyboard: true}); } keepFocus = () => { @@ -525,14 +529,82 @@ export default class MobileGridControls extends GridControls { ); } + toggleClueList = () => { + this.setState((state) => ({showClueList: !state.showClueList})); + }; + + handleSelectClue = (direction, number) => { + this.props.onSetDirection(direction); + const grid = this.grid; + const clueRoot = grid.getCellByNumber(number); + if (clueRoot) { + this.props.onSetSelected(clueRoot); + } + }; + + renderOnscreenKeyboard() { + if (!this.state.showKeyboard) return null; + + return ( +
+ {'QWERTYUIOP'.split('').map((letter) => ( + + ))} + {'ASDFGHJKL'.split('').map((letter) => ( + + ))} + {'ZXCVBNM'.split('').map((letter) => ( + + ))} + + +
+ ); + } + + handleKeyClick = (letter) => { + this.typeLetter(letter, false, {nextClueIfFilled: true}); + }; + + handleBackspaceClick = () => { + this.backspace(); + }; + + handleToggleDirection = () => { + this.flipDirection(); + }; + render() { + const {showClueList} = this.state; + return ( // eslint-disable-next-line react/no-string-refs
{this.renderClueBar()} - {this.renderGridContent()} +
{this.renderGridContent()}
{this.renderMobileInputs()} - {/* {this.renderMobileKeyboard()} */} + {this.renderOnscreenKeyboard()} + {showClueList && ( + + )} {this.props.enableDebug && (this.state.dbgstr || 'No message')}
); diff --git a/src/components/Player/css/clueList.css b/src/components/Player/css/clueList.css new file mode 100644 index 00000000..be46079d --- /dev/null +++ b/src/components/Player/css/clueList.css @@ -0,0 +1,13 @@ +/* Styles for clue list component */ +.mobile-grid-controls--clue-list-overlay .mobile-grid-controls--clue-list-title { + font-size: 18px; + font-weight: bold; +} + +.mobile-grid-controls--clue-list-content { + padding-bottom: 80px; /* Ensure bottom clues are visible when scrolling */ +} + +.mobile-grid-controls--clue-item-text { + flex: 1; +} diff --git a/src/components/Player/css/index.css b/src/components/Player/css/index.css index 7a0870dd..f7439711 100644 --- a/src/components/Player/css/index.css +++ b/src/components/Player/css/index.css @@ -91,18 +91,27 @@ .player--mobile--wrapper { display: flex; flex: 1; + flex-direction: column; + margin-top: 48px; /* Account for fixed toolbar */ + height: calc(100vh - 48px); + overflow: hidden; } .player--mobile { flex: 1; display: flex; flex-direction: column; - justify-content: center; + justify-content: flex-start; align-items: center; + overflow-y: auto; + padding-bottom: 200px; /* Space for keyboard */ } .player--mobile--grid { display: flex; + width: 100%; + max-width: 600px; + margin: 0 auto; } .player--mobile--list-view { diff --git a/src/components/Player/css/mobileGridControls.css b/src/components/Player/css/mobileGridControls.css index 993d004f..45484986 100644 --- a/src/components/Player/css/mobileGridControls.css +++ b/src/components/Player/css/mobileGridControls.css @@ -84,3 +84,67 @@ display: flex; align-items: center; } + +/* Clue list overlay */ +.mobile-grid-controls--clue-list-overlay { + position: fixed; + top: 48px; /* below toolbar */ + left: 0; + width: 100%; + height: calc(100% - 48px); + background: white; + z-index: 10; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.mobile-grid-controls--clue-list-header { + display: flex; + justify-content: space-between; + padding: 10px; + border-bottom: 1px solid #eee; + background: var(--main-blue-3); +} + +.mobile-grid-controls--clue-list-close { + font-size: 24px; + cursor: pointer; +} + +.mobile-grid-controls--clue-list-content { + flex: 1; + overflow-y: auto; + padding: 0 10px; +} + +.mobile-grid-controls--clue-section { + margin: 10px 0; +} + +.mobile-grid-controls--clue-section-header { + font-weight: bold; + margin-bottom: 5px; +} + +.mobile-grid-controls--clue-item { + padding: 8px 5px; + border-bottom: 1px solid #eee; + display: flex; +} + +.mobile-grid-controls--clue-item.active { + background-color: var(--main-blue-3); +} + +.mobile-grid-controls--clue-item-number { + font-weight: bold; + margin-right: 10px; + min-width: 30px; +} + +/* Grid wrapper for scrolling above keyboard */ +.grid-wrapper { + padding-bottom: 200px; /* space for keyboard */ + overflow-y: auto; +} diff --git a/src/components/Player/css/mobileKeyboard.css b/src/components/Player/css/mobileKeyboard.css index a59ccb78..8e17ea95 100644 --- a/src/components/Player/css/mobileKeyboard.css +++ b/src/components/Player/css/mobileKeyboard.css @@ -50,3 +50,50 @@ width: 40px; letter-spacing: 2px; } + +/* Onscreen keyboard styles */ +.onscreen-keyboard { + display: flex; + flex-wrap: wrap; + padding: 5px; + background: #ddd; + border-top: 1px solid #bbb; + position: fixed; + bottom: 0; + left: 0; + width: 100%; + box-sizing: border-box; + z-index: 5; +} + +.key-btn { + flex: 0 0 10%; /* roughly 10 buttons per row */ + margin: 2px; + font-size: 1.1rem; + padding: 10px 0; + border: none; + border-radius: 4px; + background: #fff; +} + +.key-btn:active { + background: #ccc; +} + +.key-btn.wide { + flex: 0 0 20%; /* wider buttons like backspace */ +} + +/* Dark mode support */ +body.dark-mode .onscreen-keyboard { + background: #555; +} + +body.dark-mode .key-btn { + background: #444; + color: #fff; +} + +body.dark-mode .key-btn:active { + background: #666; +} diff --git a/src/components/Toolbar/css/index.css b/src/components/Toolbar/css/index.css index 6441d173..4c87bf15 100644 --- a/src/components/Toolbar/css/index.css +++ b/src/components/Toolbar/css/index.css @@ -112,6 +112,11 @@ code { justify-content: space-between; font-size: 14pt; color: white; + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 10; } .toolbar--mobile a:first-child { @@ -139,6 +144,7 @@ code { align-items: center; position: relative; text-align: center; + width: 100%; } .toolbar--mobile .clock { @@ -148,3 +154,27 @@ code { .toolbar--mobile .toolbar--expand:last-child { margin-right: 16px; } + +/* Mobile Menu Styles */ +.toolbar--mobile-menu { + position: fixed; + top: 48px; + left: 0; + width: 100%; + background-color: #f8f8f8; + z-index: 9; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + display: flex; + flex-direction: column; + padding: 10px 0; +} + +.toolbar--mobile-menu-item { + padding: 12px 16px; + font-size: 16px; + cursor: pointer; +} + +.toolbar--mobile-menu-item:active { + background-color: #eee; +} diff --git a/src/components/Toolbar/index.js b/src/components/Toolbar/index.js index 756d95bb..4b1b0666 100644 --- a/src/components/Toolbar/index.js +++ b/src/components/Toolbar/index.js @@ -229,18 +229,11 @@ export default class Toolbar extends Component { renderListViewButton() { const {listMode, mobile} = this.props; if (mobile) { - if (listMode) { - return ( - - ); - } return ( ); } diff --git a/src/global.css b/src/global.css new file mode 100644 index 00000000..0bb76710 --- /dev/null +++ b/src/global.css @@ -0,0 +1,38 @@ +/* Global resets for mobile app feel */ +html, body { + height: 100%; + margin: 0; + padding: 0; + overscroll-behavior: none; /* Prevent bounce scroll on mobile */ + -webkit-overflow-scrolling: none; /* Disable momentum scroll outside content */ +} + +#root { + height: 100%; +} + +body { + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-touch-callout: none; + font-family: sans-serif; + font-size: 14px; + line-height: 1.4; +} + +@media (max-width: 600px) { + body { + font-size: 16px; + } +} + +/* Dark mode considerations */ +body.dark-mode .toolbar--mobile { + background: #222; + color: #fff; +} +body.dark-mode .toolbar--mobile-menu { + background: #333; + color: #fff; +} diff --git a/src/index.js b/src/index.js index 690dc8f8..bb3ba55d 100644 --- a/src/index.js +++ b/src/index.js @@ -23,6 +23,7 @@ import GlobalContext from './lib/GlobalContext'; import './style.css'; import './dark.css'; +import './global.css'; const darkModeLocalStorageKey = 'dark_mode_preference'; diff --git a/src/lib/jsUtils.js b/src/lib/jsUtils.js index 3d39b76e..da885408 100644 --- a/src/lib/jsUtils.js +++ b/src/lib/jsUtils.js @@ -48,7 +48,7 @@ export function colorAverage(hex1, hex2, weight) { if (typeof window !== 'undefined') { window.requestIdleCallback = window.requestIdleCallback || - function(cb) { + function (cb) { const start = Date.now(); return setTimeout(() => { cb({ @@ -62,7 +62,7 @@ if (typeof window !== 'undefined') { window.cancelIdleCallback = window.cancelIdleCallback || - function(id) { + function (id) { clearTimeout(id); }; } @@ -119,6 +119,10 @@ function isAncestor(a, b) { } function isMobile() { + if (typeof window !== 'undefined' && window.innerWidth < 600) { + return true; + } + if (navigator.userAgent.match(/Tablet|iPad/i)) { // do tablet stuff return true;