{this.renderMiniRooms()}
diff --git a/play.pokemonshowdown.com/src/panel-rooms.tsx b/play.pokemonshowdown.com/src/panel-rooms.tsx
index b8b316921..b8e05a910 100644
--- a/play.pokemonshowdown.com/src/panel-rooms.tsx
+++ b/play.pokemonshowdown.com/src/panel-rooms.tsx
@@ -23,7 +23,7 @@ class RoomsPanel extends PSRoomPanel {
static readonly routes = ['rooms'];
static readonly Model = RoomsRoom;
static readonly location = 'right';
- static readonly icon = ;
+ static readonly icon = ;
hidden = false;
search = '';
lastKeyCode = 0;
diff --git a/play.pokemonshowdown.com/src/panel-topbar.tsx b/play.pokemonshowdown.com/src/panel-topbar.tsx
index 73f175536..6efec422b 100644
--- a/play.pokemonshowdown.com/src/panel-topbar.tsx
+++ b/play.pokemonshowdown.com/src/panel-topbar.tsx
@@ -10,7 +10,7 @@
*/
import preact from "../js/lib/preact";
-import { PS, type RoomID } from "./client-main";
+import { PS, type PSRoom, type RoomID } from "./client-main";
import { PSMain } from "./panels";
import type { Battle } from "./battle";
import { BattleLog } from "./battle-log";
@@ -77,23 +77,13 @@ export class PSHeader extends preact.Component<{ style: object }> {
PS.dragging = { type: 'room', roomid };
};
- renderRoomTab(id: RoomID) {
- const room = PS.rooms[id];
- if (!room) return null;
- const closable = (id === '' || id === 'rooms' ? '' : ' closable');
- const cur = PS.isVisible(room) ? ' cur' : '';
- const notifying = room.notifications.length ? ' notifying' : room.isSubtleNotifying ? ' subtle-notifying' : '';
+ static roomInfo(room: PSRoom) {
const RoomType = PS.roomTypes[room.type];
- let className = `roomtab button${notifying}${closable}${cur}`;
let icon = RoomType?.icon || ;
let title = room.title;
- let closeButton = null;
switch (room.type) {
- case 'rooms':
- title = '';
- break;
case 'battle':
- let idChunks = id.substr(7).split('-');
+ let idChunks = room.id.slice(7).split('-');
let formatName;
// TODO: relocate to room implementation
if (idChunks.length <= 1) {
@@ -127,6 +117,20 @@ export class PSHeader extends preact.Component<{ style: object }> {
}
break;
}
+ return { icon, title };
+ }
+ renderRoomTab(id: RoomID) {
+ const room = PS.rooms[id];
+ if (!room) return null;
+ const closable = (id === '' || id === 'rooms' ? '' : ' closable');
+ const cur = PS.isVisible(room) ? ' cur' : '';
+ const notifying = room.notifications.length ? ' notifying' : room.isSubtleNotifying ? ' subtle-notifying' : '';
+ let className = `roomtab button${notifying}${closable}${cur}`;
+
+ let { icon, title } = PSHeader.roomInfo(room);
+ if (room.type === 'rooms') title = '';
+
+ let closeButton = null;
if (closable) {
closeButton =
@@ -197,4 +249,14 @@ export class PSHeader extends preact.Component<{ style: object }> {
}
}
+export class PSMiniHeader extends preact.Component {
+ override render() {
+ if (PS.leftPanelWidth !== null) return null;
+
+ const minWidth = Math.min(500, Math.max(320, document.body.offsetWidth - 9));
+ const { icon, title } = PSHeader.roomInfo(PS.panel);
+ return {icon} {title}
;
+ }
+}
+
preact.render({props.children}
;
}
return
{props.children}
;
}
- if (room.location !== 'left' && room.location !== 'right') {
+ if (PS.isPopup(room)) {
const style = PSMain.getPopupStyle(room, props.width);
return
{props.children}
;
}
- const style = PSMain.posStyle(room);
+ const style = PSMain.posStyle(room) as any;
+ if (props.scrollable === 'hidden') style.overflow = 'hidden';
return
@@ -260,6 +264,13 @@ export class PSMain extends preact.Component {
super();
PS.subscribe(() => this.forceUpdate());
+ if (navigator.userAgent.includes(' Safari/')) {
+ // I don't want to prevent users from being able to zoom, but iOS Safari
+ // auto-zooms when focusing textboxes (unless the font size is 16px),
+ // and this apparently fixes it while still allowing zooming.
+ document.querySelector('meta[name=viewport]')?.setAttribute('content', 'width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0');
+ }
+
window.addEventListener('click', e => {
let elem = e.target as HTMLElement | null;
if (elem?.className === 'ps-overlay') {
@@ -300,6 +311,9 @@ export class PSMain extends preact.Component {
parentElem: elem,
location,
});
+ if (!PS.isPopup(PS.rooms[roomid])) {
+ PS.closeAllPopups();
+ }
e.preventDefault();
e.stopImmediatePropagation();
}
@@ -343,8 +357,8 @@ export class PSMain extends preact.Component {
}
if (PS.room !== clickedRoom) {
if (clickedRoom) PS.room = clickedRoom;
- // eslint-disable-next-line no-unmodified-loop-condition
- while (PS.popups.length && (!clickedRoom || clickedRoom.id !== PS.popups[PS.popups.length - 1])) {
+ for (let i = PS.popups.length - 1; i >= 0; i--) {
+ if (clickedRoom && clickedRoom.id === PS.popups[i]) break;
PS.closePopup();
}
PS.update();
@@ -437,6 +451,17 @@ export class PSMain extends preact.Component {
const room = PS.getRoom(ev.target as HTMLElement, true);
if (!room) return;
+ if (document.documentElement.scrollWidth > document.documentElement.clientWidth && window.scrollX === 0) {
+ if (navigator.userAgent.includes(' Safari/') && PS.leftPanelWidth === null) {
+ // Safari is buggy here and requires temporarily disabling scroll snap
+ document.documentElement.classList.remove('vertical-header-layout');
+ window.scrollBy(400, 0);
+ setTimeout(() => document.documentElement.classList.add('vertical-header-layout'));
+ } else {
+ // intentionally around twice as big as necessary
+ window.scrollBy(400, 0);
+ }
+ }
if (window.getSelection?.()?.type === 'Range') return;
ev.preventDefault();
PS.setFocus(room);
@@ -486,47 +511,22 @@ export class PSMain extends preact.Component {
BattleTooltips.hideTooltip();
}
static posStyle(room: PSRoom) {
- let pos: PanelPosition | null = null;
- if (PS.leftPanelWidth === 0) {
+ if (PS.leftPanelWidth === null) {
+ // vertical mode
+ if (room === PS.panel) {
+ const minWidth = Math.min(500, Math.max(320, document.body.offsetWidth - 9));
+ return { top: '25px', left: '200px', minWidth: `${minWidth}px` };
+ }
+ } else if (PS.leftPanelWidth === 0) {
// one panel visible
- if (room === PS.panel) pos = { top: 56 };
+ if (room === PS.panel) return {};
} else {
// both panels visible
- if (room === PS.leftPanel) pos = { top: 56, right: PS.leftPanelWidth };
- if (room === PS.rightPanel) pos = { top: 56, left: PS.leftPanelWidth };
+ if (room === PS.leftPanel) return { width: `${PS.leftPanelWidth}px`, right: 'auto' };
+ if (room === PS.rightPanel) return { top: 56, left: PS.leftPanelWidth + 1 };
}
- if (!pos) return { display: 'none' };
-
- let top: number | null = (pos.top || 0);
- let height: number | null = null;
- let bottom: number | null = (pos.bottom || 0);
- if (bottom > 0 || top < 0) {
- height = bottom - top;
- if (height < 0) throw new RangeError("Invalid pos range");
- if (top < 0) top = null;
- else bottom = null;
- }
-
- let left: number | null = (pos.left || 0);
- let width: number | null = null;
- let right: number | null = (pos.right || 0);
- if (right > 0 || left < 0) {
- width = right - left - 1;
- if (width < 0) throw new RangeError("Invalid pos range");
- if (left < 0) left = null;
- else right = null;
- }
-
- return {
- display: 'block',
- top: top === null ? `auto` : `${top}px`,
- height: height === null ? `auto` : `${height}px`,
- bottom: bottom === null ? `auto` : `${-bottom}px`,
- left: left === null ? `auto` : `${left}px`,
- width: width === null ? `auto` : `${width}px`,
- right: right === null ? `auto` : `${-right}px`,
- };
+ return { display: 'none' };
}
static getPopupStyle(room: PSRoom, width?: number | 'auto'): any {
if (room.location === 'modal-popup' || !room.parentElem) {
@@ -547,49 +547,68 @@ export class PSMain extends preact.Component {
position: 'absolute',
margin: 0,
};
- let offset = room.parentElem.getBoundingClientRect();
- let sourceWidth = offset.width;
- let sourceHeight = offset.height;
+ // semimodal popups exist in a fixed-positioned overlay and are
+ // positioned relative to the overlay (the viewport).
+ // regular popups are positioned relative to the document root, and so
+ // need to account for scrolling.
+ const isFixed = room.location !== 'popup';
+ const offsetLeft = isFixed ? 0 : window.scrollX;
+ const offsetTop = isFixed ? 0 : window.scrollY;
+ const availableWidth = document.documentElement.clientWidth + offsetLeft;
+ const availableHeight = document.documentElement.clientHeight;
- let availableHeight = document.documentElement.clientHeight;
- let height = room.height;
+ const source = room.parentElem.getBoundingClientRect();
+ const sourceWidth = source.width;
+ const sourceHeight = source.height;
+ const sourceTop = source.top + offsetTop;
+ const sourceLeft = source.left + offsetLeft;
+
+ const height = room.height;
width = width || room.width;
if (room.rightPopup) {
- if (availableHeight > offset.top + height + 5 &&
- (offset.top < availableHeight * 2 / 3 || offset.top + 200 < availableHeight)) {
- style.top = offset.top;
- } else if (offset.top + sourceHeight >= height) {
- style.bottom = Math.max(availableHeight - offset.top - sourceHeight, 0);
+ if (availableHeight > sourceTop + height + 5 &&
+ (sourceTop < availableHeight * 2 / 3 || sourceTop + 200 < availableHeight)) {
+ style.top = sourceTop;
+ } else if (sourceTop + sourceHeight >= height) {
+ style.bottom = Math.max(availableHeight - sourceTop - sourceHeight, 0);
} else {
style.top = Math.max(0, availableHeight - height);
}
- let offsetLeft = offset.left + sourceWidth;
- if (width !== 'auto' && offsetLeft + width > document.documentElement.clientWidth) {
- style.right = 1;
+ const popupLeft = sourceLeft + sourceWidth;
+ if (width !== 'auto' && popupLeft + width > availableWidth) {
+ // can't fit, give up and put it in the normal place
+ style = {
+ position: 'absolute',
+ margin: 0,
+ };
} else {
- style.left = offsetLeft;
+ style.left = popupLeft;
}
- } else {
+ }
- if (availableHeight > offset.top + sourceHeight + height + 5 &&
- (offset.top + sourceHeight < availableHeight * 2 / 3 || offset.top + sourceHeight + 200 < availableHeight)) {
- style.top = offset.top + sourceHeight;
- } else if (height + 5 <= offset.top) {
- style.bottom = Math.max(availableHeight - offset.top, 0);
+ if (style.left === undefined) {
+
+ if (availableHeight > sourceTop + sourceHeight + height + 5 &&
+ (sourceTop + sourceHeight < availableHeight * 2 / 3 || sourceTop + sourceHeight + 200 < availableHeight)) {
+ style.top = sourceTop + sourceHeight;
+ } else if (height + 5 <= sourceTop) {
+ style.bottom = Math.max(availableHeight - sourceTop, 0);
} else if (height + 10 < availableHeight) {
style.bottom = 5;
} else {
style.top = 0;
}
- let availableWidth = document.documentElement.clientWidth - offset.left;
- if (width !== 'auto' && availableWidth < width + 10) {
- style.right = 10;
+ const availableAlignedWidth = availableWidth - sourceLeft;
+ if (width !== 'auto' && availableAlignedWidth < width + 10) {
+ // while `right: 10` would be simpler, it doesn't work if there is horizontal scrolling,
+ // like in the mobile layout
+ style.left = Math.max(availableWidth - width - 10, offsetLeft);
} else {
- style.left = offset.left;
+ style.left = sourceLeft;
}
}
@@ -617,20 +636,19 @@ export class PSMain extends preact.Component {
let rooms = [] as preact.VNode[];
for (const roomid in PS.rooms) {
const room = PS.rooms[roomid]!;
- if (room.location === 'left' || room.location === 'right') {
+ if (PS.isNormalRoom(room)) {
rooms.push(this.renderRoom(room));
}
}
return Showdown!
-
+
+
{rooms}
{PS.popups.map(roomid => this.renderPopup(PS.rooms[roomid]!))}
;
}
}
-type PanelPosition = { top?: number, bottom?: number, left?: number, right?: number } | null;
-
export function SanitizedHTML(props: { children: string }) {
return ;
}
diff --git a/play.pokemonshowdown.com/style/client2.css b/play.pokemonshowdown.com/style/client2.css
index 0cbe2fea6..58a03e0be 100644
--- a/play.pokemonshowdown.com/style/client2.css
+++ b/play.pokemonshowdown.com/style/client2.css
@@ -34,20 +34,21 @@ body {
}
.dark .tabbar a.button,
-.dark .tabbar a.button:hover {
+.dark .tabbar a.button:hover,
+.dark .header-vertical .tablist a.button,
+.dark .header-vertical .tablist a.button:hover {
box-shadow: inset 0.5px 1px 1px rgba(255, 255, 255, 0.5);
}
.dark .tabbar a.button:active,
.dark .tabbar a.button.cur,
.dark .tabbar a.button.cur:hover,
-.dark .tabbar a.button.cur:active {
+.dark .tabbar a.button.cur:active,
+.dark .header-vertical .tablist a.button:active,
+.dark .header-vertical .tablist a.button.cur,
+.dark .header-vertical .tablist a.button.cur:hover,
+.dark .header-vertical .tablist a.button.cur:active {
box-shadow: none;
}
-.dark .maintabbarbottom {
- background: #555555;
- border-color: #5A5A5A;
- border-top-color: #34373b;
-}
pre {
white-space: pre-wrap;
@@ -58,6 +59,16 @@ pre {
* Header
*********************************************************/
+.vertical-header-layout {
+ scroll-snap-type: x mandatory;
+}
+.vertical-header-layout .header-vertical {
+ scroll-snap-align: start;
+}
+.vertical-header-layout .mini-header {
+ scroll-snap-align: end;
+}
+
.header {
position: relative;
height: 50px;
@@ -71,6 +82,17 @@ pre {
float: left;
margin: 0;
}
+.header-vertical {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 194px;
+ background: rgba(255,255,255,.3);
+}
+.dark .header, .dark .header-vertical {
+ background: rgba(0,0,0,.30);
+}
.userbar {
position: absolute;
@@ -78,10 +100,22 @@ pre {
right: 12px;
font-weight: bold;
}
+.header-vertical .userbar {
+ right: auto;
+ top: auto;
+ left: 12px;
+ bottom: 12px;
+ width: 170px;
+ word-wrap: break-word;
+}
.userbar .username {
color: black;
text-shadow: 1px 1px 0 #f8f8f8, 1px -1px 0 #f8f8f8, -1px 1px 0 #f8f8f8, -1px -1px 0 #f8f8f8;
}
+.dark .userbar .username {
+ color: #DDD;
+ text-shadow: 1px 1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, -1px -1px 0 #000;
+}
.username {
cursor: pointer;
}
@@ -89,6 +123,9 @@ pre {
.usernametext {
display: none
}
+ .header-vertical .usernametext {
+ display: inline;
+ }
}
.userbar button.icon {
height: 25px;
@@ -120,6 +157,11 @@ pre {
line-height: 100%;
}
+.mini-header, .maintabbarbottom {
+ background: #f8f8f8;
+ border: solid 1px #AAAAAA;
+ color: black;
+}
.maintabbarbottom {
content: "";
display: block;
@@ -128,15 +170,58 @@ pre {
right: 0;
bottom: -6px;
height: 6px;
- background: #f8f8f8;
- border: solid 1px #AAAAAA;
- border-left: 0;
- border-right: 0;
+ border-left-width: 0;
+ border-right-width: 0;
margin: -1px 0 0 0;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2);
-moz-box-shadow: 0 1px 2px rgba(0,0,0,.2);
box-shadow: 0 1px 2px rgba(0,0,0,.2);
}
+.mini-header {
+ position: absolute;
+ left: 199px;
+ right: 0;
+ top: 0;
+ padding: 0;
+ height: 24px;
+ line-height: 24px;
+ border-left: 0;
+ border-right: 0;
+ border-top: 0;
+}
+.mini-header i.text {
+ font-size: 12px;
+ font-style: normal;
+ border: 1px solid #777;
+ padding: 1px 3px;
+ border-radius: 5px;
+ vertical-align: middle;
+ margin-top: -2px;
+ display: inline-block;
+ line-height: 12px;
+}
+.header-vertical .maintabbarbottom {
+ top: 0;
+ bottom: 0;
+ left: 194px;
+ right: auto;
+ width: 6px;
+ height: auto;
+ border-top: 0;
+ border-bottom: 0;
+ border-left-width: 1px;
+ border-right-width: 1px;
+ margin: 0 0 0 -1px;
+}
+.dark .mini-header, .dark .maintabbarbottom {
+ background: #555555;
+ border-color: #34373b;
+ color: #CCCCCC;
+}
+.dark .maintabbarbottom {
+ border-bottom-color: #5A5A5A;
+}
+
.tabbar.maintabbar {
margin-left: 52px;
margin-right: 165px;
@@ -201,6 +286,9 @@ pre {
font-size: 14px;
height: 14px;
}
+.tabbar a.button i.rooms-plus {
+ margin: 7px auto -6px auto;
+}
.tabbar a.button span {
display: block;
overflow: hidden;
@@ -303,10 +391,11 @@ span.header-username:hover {
padding: 0;
}
.tablist ul {
- padding: 3px 0 4px 0;
+ padding: 3px 0 4px 5px;
}
.tablist li {
- margin: 0 -5px -1px -5px;
+ clear: both;
+ margin: -1px 0 0 0;
padding: 0;
}
.tablist .button {
@@ -331,14 +420,26 @@ span.header-username:hover {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
+.header-vertical .tablist li .button {
+ width: auto;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-right-width: 0;
+ margin: 0 1px 0 2px;
+ box-shadow: none;
+}
+.header-vertical .tablist li .button.cur {
+ margin: 0;
+}
.tablist .closebutton {
display: block;
float: right;
margin-top: -28px;
+ margin-right: 4px;
padding: 5px 0 0 0;
height: 23px;
font-size: 14px;
- width: 28px;
+ width: 24px;
text-align: center;
position: relative;
@@ -386,7 +487,7 @@ span.header-username:hover {
}
.ps-overlay {
- position: absolute;
+ position: fixed;
top: 0;
left: 0;
right: 0;
@@ -550,7 +651,7 @@ p.or:after {
width: 292px;
}
.tiny-layout .mini-window {
- margin: 0 0 12px 0;
+ margin: 0 auto 12px;
}
.mini-window h3 {
background: rgba(248,248,248,.8);
@@ -1442,7 +1543,7 @@ pre.textbox.textbox-empty[placeholder]:before {
transform-origin: top left;
-webkit-transform-origin: top left;
}
-.ps-room .battle-controls {
+.battle-controls {
position: absolute;
top: 370px;
left: 0;
@@ -1451,12 +1552,13 @@ pre.textbox.textbox-empty[placeholder]:before {
.battle-chat-toggle {
display: none;
}
-.tiny-layout.ps-room .battle-controls {
- left: 0;
- right: 0;
+.battle-log .battle-controls {
+ top: 0;
+ position: static;
width: auto;
+ padding-bottom: 6px;
}
-.tiny-layout .movecontrols, .tiny-layout .shiftcontrols, .tiny-layout .switchcontrols {
+.battle-log .movecontrols, .battle-log .shiftcontrols, .battle-log .switchcontrols {
max-width: 330px;
margin: 0 auto;
}
@@ -1736,6 +1838,11 @@ pre.textbox.textbox-empty[placeholder]:before {
margin-right: -10px;
padding-left: 4px;
}
+.switchmenu:after {
+ content: '';
+ display: block;
+ clear: both;
+}
.switchmenu button,
.movebutton {
position: relative;
@@ -2033,10 +2140,6 @@ pre.textbox.textbox-empty[placeholder]:before {
* Dark mode!
*********************************************************/
-.dark .header {
- background: rgba(0,0,0,.30);
-}
-
.dark .ps-room.ps-room-light,
.dark .ps-room-light,
.dark .tournament-box,
@@ -2087,11 +2190,6 @@ pre.textbox.textbox-empty[placeholder]:before {
backdrop-filter: blur(4px);
}
-.dark .userbar .username {
- color: #DDD;
- text-shadow: 1px 1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, -1px -1px 0 #000;
-}
-
/* popups */
.dark .ps-popup {
diff --git a/play.pokemonshowdown.com/testclient-beta.html b/play.pokemonshowdown.com/testclient-beta.html
index 55ea1fec6..cea4e103c 100644
--- a/play.pokemonshowdown.com/testclient-beta.html
+++ b/play.pokemonshowdown.com/testclient-beta.html
@@ -1,7 +1,7 @@
-
+
-
+