Preact minor update batch 11
Some checks are pending
Node.js CI / build (22.x) (push) Waiting to run

Minor
- Unhide right panel when choosing "Two panels" layout option
- Refactor focusing
  - Correctly focus next room when closing currently active room
  - Correctly focus room when joining new room
- Use strict mode on all compiled files
- Fix router when started on `/` (it previously required starting on a
  non-empty room ID, which wasn't noticeable back when the URL needed
  to be `/preactalpha`)
- Update teambuilder sidebar CSS, to make it easier to add regular text
  - This is mainly for the "Tournaments" button in the main menu,
    which shares the CSS
- Fix new tournament elim tree text in Safari
- Update new tournament elim tree highlighted links to reliably
  link every still-playing game
- Remove latest gen from format name displays everywhere
  - Previously, they would only be removed from the format dropdown,
    but now they're also gone from the Ladder tab, battle tabs, and
    `/rank`
- Support async d3 loading
  - This allows chatrooms to be loaded way before all our dependencies
    are fully downloaded
- Remove "[Gen 9]" from format names everywhere (previously it was only
  removed from the format dropdown)
  - Also add "[Gen 6]" to unlabeled formats in `/rank` (Gen 6 was the
    last time we didn't have format generation as part of format names)

Trivial
- Stricter JSX linting
  - (unfortunately, most of the JSX style enforcement I actually want
    isn't possible in @stylistic)
- Make room.subscribeTo's second parameter optional
- Rearrange and comment loading order
- Rename hiddenInit -> focusNextUpdate (clarity)
- Rename PSMain -> PSView (clarity)
- Fix button spacing in Change Password
- Add `touch-action: manipulation` to <a> tags
- Refactor `nodeSize` in elim tour trees
This commit is contained in:
Guangcong Luo 2025-04-17 11:09:12 +00:00
parent 7ee7c6604a
commit 5971e5151a
26 changed files with 453 additions and 403 deletions

View File

@ -29,7 +29,9 @@
// ES3 // ES3
"@babel/plugin-transform-member-expression-literals", "@babel/plugin-transform-member-expression-literals",
"@babel/plugin-transform-property-literals" "@babel/plugin-transform-property-literals",
"@babel/plugin-transform-strict-mode"
], ],
"ignore": [ "ignore": [
"src/globals.d.ts" "src/globals.d.ts"

View File

@ -25,3 +25,6 @@ trim_trailing_whitespace = false
indent_style = tab indent_style = tab
indent_size = 8 indent_size = 8
trim_trailing_whitespace = false trim_trailing_whitespace = false
[COMMIT_EDITMSG]
indent_style = space

View File

@ -174,6 +174,7 @@ export const defaultRules = {
"@stylistic/jsx-one-expression-per-line": "off", "@stylistic/jsx-one-expression-per-line": "off",
"@stylistic/jsx-max-props-per-line": "off", "@stylistic/jsx-max-props-per-line": "off",
"@stylistic/jsx-function-call-newline": "off", "@stylistic/jsx-function-call-newline": "off",
"@stylistic/jsx-child-element-spacing": "error",
"no-restricted-syntax": ["error", "no-restricted-syntax": ["error",
{ selector: "CallExpression[callee.name='Symbol']", message: "Annoying to serialize, just use a string" }, { selector: "CallExpression[callee.name='Symbol']", message: "Annoying to serialize, just use a string" },
], ],

16
package-lock.json generated
View File

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@babel/core": "^7.26.10", "@babel/core": "^7.26.10",
"@babel/plugin-transform-react-jsx": "^7.25.9", "@babel/plugin-transform-react-jsx": "^7.25.9",
"@babel/plugin-transform-strict-mode": "^7.25.9",
"@babel/preset-env": "^7.26.9", "@babel/preset-env": "^7.26.9",
"@babel/preset-typescript": "^7.27.0", "@babel/preset-typescript": "^7.27.0",
"babel-plugin-remove-import-export": "^1.1.1", "babel-plugin-remove-import-export": "^1.1.1",
@ -1264,6 +1265,21 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/plugin-transform-strict-mode": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-strict-mode/-/plugin-transform-strict-mode-7.25.9.tgz",
"integrity": "sha512-DplEwkN9xt6XCz/4oC9l8FJGn7LnOGPU7v08plq+OclMT55zAR9lkX7QIbQ9XscvvJNYpLUfYO4IYz/7JGkbXQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-template-literals": { "node_modules/@babel/plugin-transform-template-literals": {
"version": "7.26.8", "version": "7.26.8",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz",

View File

@ -18,6 +18,7 @@
"dependencies": { "dependencies": {
"@babel/core": "^7.26.10", "@babel/core": "^7.26.10",
"@babel/plugin-transform-react-jsx": "^7.25.9", "@babel/plugin-transform-react-jsx": "^7.25.9",
"@babel/plugin-transform-strict-mode": "^7.25.9",
"@babel/preset-env": "^7.26.9", "@babel/preset-env": "^7.26.9",
"@babel/preset-typescript": "^7.27.0", "@babel/preset-typescript": "^7.27.0",
"babel-plugin-remove-import-export": "^1.1.1", "babel-plugin-remove-import-export": "^1.1.1",

View File

@ -612,8 +612,8 @@
TournamentBox.nodeSize = { TournamentBox.nodeSize = {
width: 160, height: 30, width: 160, height: 30,
radius: 5, radius: 5,
separationX: 20, separationY: 20, separationX: 20, separationY: 10,
textOffset: -1 textOffset: 4
}; };
TournamentBox.prototype.generateBracket = function (data, abbreviated) { TournamentBox.prototype.generateBracket = function (data, abbreviated) {
@ -655,9 +655,15 @@
child.highlightLink = true; child.highlightLink = true;
} }
} }
} else if (node.state === 'inprogress' || node.state === 'available' || node.state === 'challenging') { } else if (
node.state === 'inprogress' || node.state === 'available' || node.state === 'challenging' ||
node.state === 'unavailable'
) {
for (var i = 0; i < node.children.length; i++) { for (var i = 0; i < node.children.length; i++) {
node.children[i].highlightLink = true; var child = node.children[i];
if (child.team && !child.team.startsWith('(')) {
child.highlightLink = true;
}
} }
} else if (highlightName) { } else if (highlightName) {
for (var i = 0; i < node.children.length; i++) { for (var i = 0; i < node.children.length; i++) {
@ -763,6 +769,7 @@
if (node.team === name) rect.attr('stroke-dasharray', '5,5').attr('stroke-width', 2); if (node.team === name) rect.attr('stroke-dasharray', '5,5').attr('stroke-width', 2);
elem.append('svg:text').classed('tournament-bracket-tree-node-team', true) elem.append('svg:text').classed('tournament-bracket-tree-node-team', true)
.attr('y', nodeSize.textOffset)
.classed('tournament-bracket-tree-node-team-draw', true) .classed('tournament-bracket-tree-node-team-draw', true)
.text(node.team || ''); .text(node.team || '');
} else { } else {

View File

@ -0,0 +1,4 @@
"use strict";
PS.libsLoaded.loaded();
//# sourceMappingURL=client-endload.js.map

View File

@ -1339,11 +1339,6 @@
bufs[curBuf] += BattleLog.escapeHTML(curSection) + '</strong></summary>'; bufs[curBuf] += BattleLog.escapeHTML(curSection) + '</strong></summary>';
} }
var formatName = BattleLog.escapeFormat(format.id); var formatName = BattleLog.escapeFormat(format.id);
if (formatName.charAt(0) !== '[') formatName = '[Gen 6] ' + formatName;
formatName = formatName.replace('[Gen 9] ', '');
formatName = formatName.replace('[Gen 9 ', '[');
formatName = formatName.replace('[Gen 8 ', '[');
formatName = formatName.replace('[Gen 7 ', '[');
bufs[curBuf] += ( bufs[curBuf] += (
'<li><button name="selectFormat" value="' + i + '<li><button name="selectFormat" value="' + i +
'" class="option' + (curFormat === i ? ' cur' : '') + '">' + formatName + '" class="option' + (curFormat === i ? ' cur' : '') + '">' + formatName +

View File

@ -119,7 +119,7 @@
if (room.title && room.title.charAt(0) === '[') { if (room.title && room.title.charAt(0) === '[') {
var closeBracketIndex = room.title.indexOf(']'); var closeBracketIndex = room.title.indexOf(']');
if (closeBracketIndex > 0) { if (closeBracketIndex > 0) {
return buf + ' draggable="true"><i class="text">' + BattleLog.escapeFormat(room.title.slice(1, closeBracketIndex)) + '</i><span>' + BattleLog.escapeHTML(room.title.slice(closeBracketIndex + 1)) + '</span></a><button class="closebutton" name="closeRoom" value="' + id + '" aria-label="Close"><i class="fa fa-times-circle"></i></a></li>'; return buf + ' draggable="true"><i class="text">' + BattleLog.escapeHTML(room.title.slice(1, closeBracketIndex)) + '</i><span>' + BattleLog.escapeHTML(room.title.slice(closeBracketIndex + 1)) + '</span></a><button class="closebutton" name="closeRoom" value="' + id + '" aria-label="Close"><i class="fa fa-times-circle"></i></a></li>';
} }
} }
return buf + ' draggable="true"><i class="fa fa-file-text-o"></i> <span>' + (BattleLog.escapeHTML(room.title) || id) + '</span></a><button class="closebutton" name="closeRoom" value="' + id + '" aria-label="Close"><i class="fa fa-times-circle"></i></a></li>'; return buf + ' draggable="true"><i class="fa fa-file-text-o"></i> <span>' + (BattleLog.escapeHTML(room.title) || id) + '</span></a><button class="closebutton" name="closeRoom" value="' + id + '" aria-label="Close"><i class="fa fa-times-circle"></i></a></li>';

View File

@ -72,13 +72,14 @@ https://psim.us/dev
document.head.appendChild(linkEl); document.head.appendChild(linkEl);
} }
linkStyle("/style/sim-types.css"); linkStyle("/style/sim-types.css");
linkStyle("/style/teambuilder.css"); linkStyle("/style/teambuilder.css?");
linkStyle("style/battle-search.css"); linkStyle("style/battle-search.css");
linkStyle("/style/font-awesome.css"); linkStyle("/style/font-awesome.css");
</script> </script>
<script nomodule defer src="/js/lib/ps-polyfill.js"></script> <script nomodule defer src="/js/lib/ps-polyfill.js"></script>
<script defer src="/config/config.js?"></script> <script defer src="/config/config.js?"></script>
<script defer src="/js/client-core.js?"></script> <script defer src="/js/client-core.js?"></script>
<!-- At this point, background and dark mode are loaded -->
<script defer src="/js/battle-dex-data.js?"></script> <script defer src="/js/battle-dex-data.js?"></script>
<script defer src="/js/battle-dex.js?"></script> <script defer src="/js/battle-dex.js?"></script>
@ -97,10 +98,16 @@ https://psim.us/dev
<script defer src="/js/panel-mainmenu.js?"></script> <script defer src="/js/panel-mainmenu.js?"></script>
<script defer src="/js/panel-rooms.js?"></script> <script defer src="/js/panel-rooms.js?"></script>
<script defer src="/js/panel-topbar.js?"></script> <script defer src="/js/panel-topbar.js?"></script>
<script defer src="/js/panel-popups.js?"></script> <!-- at this point, the main view is loaded and usable -->
<script defer src="/js/miniedit.js?"></script> <script defer src="/js/miniedit.js?"></script>
<script defer src="/js/panel-chat-tournament.js?"></script> <script defer src="/js/panel-chat-tournament.js?"></script>
<script defer src="/js/panel-chat.js?"></script> <script defer src="/js/panel-chat.js?"></script>
<!-- at this point, chatrooms are usable -->
<script defer src="/js/panel-popups.js?"></script>
<script defer src="/js/panel-page.js?"></script>
<script defer src="/js/panel-ladder.js?"></script>
<script defer src="/js/battle-sound.js"></script> <script defer src="/js/battle-sound.js"></script>
<script defer src="/js/lib/jquery-2.2.4.min.js"></script> <script defer src="/js/lib/jquery-2.2.4.min.js"></script>
@ -122,11 +129,11 @@ https://psim.us/dev
<script defer src="/js/battle-dex-search.js?"></script> <script defer src="/js/battle-dex-search.js?"></script>
<script defer src="/js/battle-searchresults.js?"></script> <script defer src="/js/battle-searchresults.js?"></script>
<script defer src="/js/panel-teambuilder-team.js?"></script> <script defer src="/js/panel-teambuilder-team.js?"></script>
<script defer src="/js/panel-ladder.js?"></script>
<script defer src="/js/panel-page.js?"></script>
<script defer src="/data/pokedex-mini.js?"></script> <script defer src="/data/pokedex-mini.js?"></script>
<script defer src="/data/pokedex-mini-bw.js?"></script> <script defer src="/data/pokedex-mini-bw.js?"></script>
<script defer src="/js/lib/d3.v3.min.js"></script> <script defer src="/js/lib/d3.v3.min.js"></script>
<script defer src="/js/client-endload.js?"></script>
</body></html> </body></html>

View File

@ -1056,30 +1056,42 @@ export class BattleLog {
static escapeFormat(formatid = ''): string { static escapeFormat(formatid = ''): string {
let atIndex = formatid.indexOf('@@@'); let atIndex = formatid.indexOf('@@@');
if (atIndex >= 0) { if (atIndex >= 0) {
return this.escapeFormat(formatid.slice(0, atIndex)) + return this.escapeHTML(this.formatName(formatid.slice(0, atIndex))) +
'<br />Custom rules: ' + this.escapeHTML(formatid.slice(atIndex + 3)); '<br />Custom rules: ' + this.escapeHTML(formatid.slice(atIndex + 3));
} }
if (window.BattleFormats && BattleFormats[formatid]) { return this.escapeHTML(this.formatName(formatid));
return this.escapeHTML(BattleFormats[formatid].name);
}
if (window.NonBattleGames && NonBattleGames[formatid]) {
return this.escapeHTML(NonBattleGames[formatid]);
}
return this.escapeHTML(formatid);
} }
/**
* Do not store this output anywhere; it removes the generation number
* for the current gen.
*/
static formatName(formatid = ''): string { static formatName(formatid = ''): string {
if (!formatid) return '';
let atIndex = formatid.indexOf('@@@'); let atIndex = formatid.indexOf('@@@');
if (atIndex >= 0) { if (atIndex >= 0) {
return this.formatName(formatid.slice(0, atIndex)) + return this.formatName(formatid.slice(0, atIndex)) +
' (Custom rules: ' + this.escapeHTML(formatid.slice(atIndex + 3)) + ')'; ' (Custom rules: ' + this.escapeHTML(formatid.slice(atIndex + 3)) + ')';
} }
if (!formatid.startsWith('gen')) {
formatid = `gen6${formatid}`;
}
let name = formatid;
if (window.BattleFormats && BattleFormats[formatid]) { if (window.BattleFormats && BattleFormats[formatid]) {
return BattleFormats[formatid].name; name = BattleFormats[formatid].name;
} }
if (window.NonBattleGames && NonBattleGames[formatid]) { if (window.NonBattleGames && NonBattleGames[formatid]) {
return NonBattleGames[formatid]; name = NonBattleGames[formatid];
} }
return formatid; if (name.startsWith('gen')) {
name = name.replace(/^gen([0-9])/, '[Gen $1] ');
}
if (name.startsWith(`[Gen ${Dex.gen}] `)) {
name = name.slice(`[Gen ${Dex.gen}] `.length);
} else if (name.startsWith(`[Gen ${Dex.gen} `)) {
name = '[' + name.slice(`[Gen ${Dex.gen} `.length);
}
return name;
} }
static escapeHTML(str: string | number, jsEscapeToo?: boolean) { static escapeHTML(str: string | number, jsEscapeToo?: boolean) {

View File

@ -0,0 +1,3 @@
import { PS } from "./client-main";
PS.libsLoaded.loaded();

View File

@ -696,6 +696,18 @@ type ParsedClientCommands = {
[command: `parsed${string}`]: (this: PSRoom, target: string, cmd: string) => string | boolean | null | void, [command: `parsed${string}`]: (this: PSRoom, target: string, cmd: string) => string | boolean | null | void,
}; };
export class PSLoadTracker extends Promise<void> {
resolver!: (value: void) => void;
constructor() {
super(resolve => {
this.resolver = resolve;
});
}
loaded() {
this.resolver();
}
}
/** /**
* As a PSStreamModel, PSRoom can emit `Args` to mean "we received a message", * As a PSStreamModel, PSRoom can emit `Args` to mean "we received a message",
* and `null` to mean "tell Preact to re-render this room" * and `null` to mean "tell Preact to re-render this room"
@ -728,11 +740,13 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
width = 0; width = 0;
height = 0; height = 0;
/** /**
* popups sometimes initialize hidden, to calculate their position from their * Preact means that the DOM state lags behind the app state. This means
* width/height without flickering. But hidden popups can't be focused, so * rooms frequently have `display: none` at the time we want to focus them.
* we need to track their focus timing here. * And popups sometimes initialize hidden, to calculate their position from
* their width/height without flickering. But hidden HTML elements can't be
* focused, so this is a note-to-self to focus the next time they can be.
*/ */
hiddenInit = false; focusNextUpdate = false;
parentElem: HTMLElement | null = null; parentElem: HTMLElement | null = null;
parentRoomid: RoomID | null = null; parentRoomid: RoomID | null = null;
rightPopup = false; rightPopup = false;
@ -1134,6 +1148,8 @@ export const PS = new class extends PSModel {
newsHTML = document.querySelector('#room-news .mini-window-body')?.innerHTML || ''; newsHTML = document.querySelector('#room-news .mini-window-body')?.innerHTML || '';
libsLoaded = new PSLoadTracker();
constructor() { constructor() {
super(); super();
@ -1300,12 +1316,11 @@ export const PS = new class extends PSModel {
room = PS.rooms[roomid2]; room = PS.rooms[roomid2];
const [, type] = args; const [, type] = args;
if (!room) { if (!room) {
this.addRoom({ room = this.addRoom({
id: roomid2, id: roomid2,
type, type,
connected: true, connected: true,
}, roomid === 'staff' || roomid === 'upperstaff'); }, roomid === 'staff' || roomid === 'upperstaff');
room = PS.rooms[roomid2];
} else { } else {
room.type = type; room.type = type;
room.connected = true; room.connected = true;
@ -1471,8 +1486,11 @@ export const PS = new class extends PSModel {
if (this.leftPanel === room) this.leftPanel = newRoom; if (this.leftPanel === room) this.leftPanel = newRoom;
if (this.rightPanel === room) this.rightPanel = newRoom; if (this.rightPanel === room) this.rightPanel = newRoom;
if (this.panel === room) this.panel = newRoom; if (this.panel === room) this.panel = newRoom;
if (this.room === room) this.room = newRoom;
if (roomid === '') this.mainmenu = newRoom as MainMenuRoom; if (roomid === '') this.mainmenu = newRoom as MainMenuRoom;
if (this.room === room) {
this.room = newRoom;
newRoom.focusNextUpdate = true;
}
updated = true; updated = true;
} }
@ -1490,7 +1508,7 @@ export const PS = new class extends PSModel {
} }
this.closePopupsAbove(room, true); this.closePopupsAbove(room, true);
if (!this.isVisible(room)) { if (!this.isVisible(room)) {
room.hiddenInit = true; room.focusNextUpdate = true;
} }
if (PS.isNormalRoom(room)) { if (PS.isNormalRoom(room)) {
if (room.location === 'right' && !this.prefs.onepanel) { if (room.location === 'right' && !this.prefs.onepanel) {
@ -1574,42 +1592,6 @@ export const PS = new class extends PSModel {
} }
return this.focusRoom(rooms[index + 1]); return this.focusRoom(rooms[index + 1]);
} }
focusPreview(room: PSRoom) {
if (room !== this.room) return '';
const verticalBuf = this.verticalFocusPreview();
if (verticalBuf) return verticalBuf;
const isMiniRoom = this.room.location === 'mini-window';
const { rooms, index } = this.horizontalNav();
if (index === -1) return '';
let buf = ' ';
const leftRoom = this.rooms[rooms[index - 1]];
if (leftRoom) buf += `\u2190 ${leftRoom.title}`;
buf += (this.arrowKeysUsed || isMiniRoom ? " | " : " (use arrow keys) ");
const rightRoom = this.rooms[rooms[index + 1]];
if (rightRoom) buf += `${rightRoom.title} \u2192`;
return buf;
}
verticalFocusPreview() {
const { rooms, index } = this.verticalNav();
if (index === -1) return '';
const upRoom = this.rooms[rooms[index - 1]];
let downRoom = this.rooms[rooms[index + 1]];
if (index === rooms.length - 2 && rooms[index + 1] === 'news') downRoom = undefined;
if (!upRoom && !downRoom) return '';
let buf = ' ';
// const altLabel = navigator.platform?.startsWith('Mac') ? '⌥' : 'ᴀʟᴛ';
const altLabel = navigator.platform?.startsWith('Mac') ? 'ᴏᴘᴛ' : 'ᴀʟᴛ';
if (upRoom) buf += `${altLabel}\u2191 ${upRoom.title}`;
buf += " | ";
if (downRoom) buf += `${altLabel}\u2193 ${downRoom.title}`;
return buf;
}
alert(message: string) { alert(message: string) {
this.join(`popup-${this.popups.length}` as RoomID, { this.join(`popup-${this.popups.length}` as RoomID, {
args: { message }, args: { message },
@ -1676,6 +1658,7 @@ export const PS = new class extends PSModel {
room.receiveLine(args); room.receiveLine(args);
} }
} }
if (!noFocus) room.focusNextUpdate = true;
return room; return room;
} }
hideRightRoom() { hideRightRoom() {
@ -1809,6 +1792,7 @@ export const PS = new class extends PSModel {
} }
} }
removeRoom(room: PSRoom) { removeRoom(room: PSRoom) {
const wasFocused = this.room === room;
room.destroy(); room.destroy();
delete PS.rooms[room.id]; delete PS.rooms[room.id];
@ -1848,7 +1832,9 @@ export const PS = new class extends PSModel {
PS.room = this.popups.length ? PS.rooms[this.popups[this.popups.length - 1]]! : PS.panel; PS.room = this.popups.length ? PS.rooms[this.popups[this.popups.length - 1]]! : PS.panel;
} }
PS.setFocus(PS.room); if (wasFocused) {
this.room.focusNextUpdate = true;
}
} }
/** do NOT use this in a while loop: see `closePopupsUntil */ /** do NOT use this in a while loop: see `closePopupsUntil */
closePopup(skipUpdate?: boolean) { closePopup(skipUpdate?: boolean) {

View File

@ -662,7 +662,7 @@ export class TournamentBracket extends preact.Component<{
export class TournamentTreeBracket extends preact.Component<{ export class TournamentTreeBracket extends preact.Component<{
data: TournamentTreeBracketData, abbreviated?: boolean, data: TournamentTreeBracketData, abbreviated?: boolean,
}> { }> {
d3Loaded = true; d3Loader: Promise<void> | null = null;
forEachTreeNode<T extends TreeNode>(node: T, callback: (node: T, depth: number) => void, depth = 0) { forEachTreeNode<T extends TreeNode>(node: T, callback: (node: T, depth: number) => void, depth = 0) {
callback(node, depth); callback(node, depth);
if (node.children) { if (node.children) {
@ -678,11 +678,15 @@ export class TournamentTreeBracket extends preact.Component<{
} }
return clonedNode; return clonedNode;
} }
/**
* Customize tree size. Height is for a single player, a full node is double that.
*/
static nodeSize = { static nodeSize = {
width: 160, height: 30, width: 160, height: 15,
radius: 5, radius: 5,
separationX: 20, separationY: 10, separationX: 20, separationY: 10,
textOffset: -1, // Safari bug: some issue with dominant-baseline. whatever, we can just manually v-align text
textOffset: 4,
}; };
generateTreeBracket(data: TournamentTreeBracketData, abbreviated?: boolean) { generateTreeBracket(data: TournamentTreeBracketData, abbreviated?: boolean) {
const div = document.createElement('div'); const div = document.createElement('div');
@ -698,11 +702,13 @@ export class TournamentTreeBracket extends preact.Component<{
return div; return div;
} }
if (!window.d3) { if (!window.d3) {
this.d3Loaded = false;
div.innerHTML = `<b>d3 not loaded yet</b>`; div.innerHTML = `<b>d3 not loaded yet</b>`;
this.d3Loader ||= PS.libsLoaded.then(() => {
this.forceUpdate();
});
return div; return div;
} }
this.d3Loaded = true; this.d3Loader = null;
let name = PS.user.name; let name = PS.user.name;
@ -729,9 +735,14 @@ export class TournamentTreeBracket extends preact.Component<{
child.highlightLink = true; child.highlightLink = true;
} }
} }
} else if (node.state === 'inprogress' || node.state === 'available' || node.state === 'challenging') { } else if (
node.state === 'inprogress' || node.state === 'available' || node.state === 'challenging' ||
node.state === 'unavailable'
) {
for (const child of node.children) { for (const child of node.children) {
child.highlightLink = true; if (child.team && !child.team.startsWith('(')) {
child.highlightLink = true;
}
} }
} else if (highlightName) { } else if (highlightName) {
for (const child of node.children) { for (const child of node.children) {
@ -755,24 +766,26 @@ export class TournamentTreeBracket extends preact.Component<{
} }
}); });
// Setting `breadthCompression` to 0.8 for 3+ depths with leaves is
// an extremely approximate guess for how tall a double+ elim tree
// should be. The old algorithm also approximated, but its
// approximation was arguably worse. This one is basically perfect
// for single elim and pretty good for double elim.
const depthsWithLeaves = hasLeafAtDepth.filter(Boolean).length; const depthsWithLeaves = hasLeafAtDepth.filter(Boolean).length;
const breadthCompression = depthsWithLeaves > 2 ? 0.8 : 2; const breadthCompression = depthsWithLeaves > 2 ? 0.8 : 2;
const maxBreadth = numLeaves - (depthsWithLeaves - 1) / breadthCompression; const maxBreadth = numLeaves - (depthsWithLeaves - 1) / breadthCompression;
const maxDepth = hasLeafAtDepth.length; const maxDepth = hasLeafAtDepth.length;
const nodeSize: any = { ...TournamentTreeBracket.nodeSize }; const nodeSize = TournamentTreeBracket.nodeSize;
nodeSize.realWidth = nodeSize.width;
nodeSize.realHeight = nodeSize.height;
nodeSize.smallRealHeight = nodeSize.height / 2;
const size = { const size = {
width: nodeSize.realWidth * maxDepth + nodeSize.separationX * (maxDepth + 1), width: nodeSize.width * maxDepth + nodeSize.separationX * (maxDepth + 1),
height: nodeSize.realHeight * (maxBreadth + 0.5) + nodeSize.separationY * maxBreadth, height: nodeSize.height * 2 * (maxBreadth + 0.5) + nodeSize.separationY * maxBreadth,
}; };
// Make d3 layout the tree // Make d3 layout the tree
const tree = d3.layout.tree<TournamentElimBracketNode>() const tree = d3.layout.tree<TournamentElimBracketNode>()
.size([size.height, size.width - nodeSize.realWidth - nodeSize.separationX]) .size([size.height, size.width - nodeSize.width - nodeSize.separationX])
.separation(() => 1) .separation(() => 1)
.children(node => ( .children(node => (
node.children?.length ? node.children : null! node.children?.length ? node.children : null!
@ -785,16 +798,16 @@ export class TournamentTreeBracket extends preact.Component<{
const layoutRoot = d3.select(div) const layoutRoot = d3.select(div)
.append('svg:svg').attr('width', size.width).attr('height', size.height) .append('svg:svg').attr('width', size.width).attr('height', size.height)
.append('svg:g') .append('svg:g')
.attr('transform', `translate(${-(nodeSize.realWidth) / 2 - 6},0)`); .attr('transform', `translate(${-(nodeSize.width) / 2 - 6},0)`);
// Style the links between the nodes // Style the links between the nodes
const diagonalLink = d3.svg.diagonal() const diagonalLink = d3.svg.diagonal()
.source(link => ({ .source(link => ({
x: link.source.x, y: link.source.y + nodeSize.realWidth / 2, x: link.source.x, y: link.source.y + nodeSize.width / 2,
})) }))
.target(link => ({ .target(link => ({
x: link.target.x, y: link.target.y - nodeSize.realWidth / 2, x: link.target.x, y: link.target.y - nodeSize.width / 2,
})) }))
.projection(link => [ .projection(link => [
size.width - link.y, link.x, size.width - link.y, link.x,
@ -818,8 +831,8 @@ export class TournamentTreeBracket extends preact.Component<{
const outerElem = elem; const outerElem = elem;
if (node.abbreviated) { if (node.abbreviated) {
elem.append('svg:text').attr('y', -nodeSize.realHeight / 4 + 4) elem.append('svg:text').attr('y', -nodeSize.height / 2 + 4)
.attr('x', -nodeSize.realWidth / 2 - 7).classed('tournament-bracket-tree-abbreviated', true) .attr('x', -nodeSize.width / 2 - 7).classed('tournament-bracket-tree-abbreviated', true)
.text('...'); .text('...');
} }
@ -841,28 +854,29 @@ export class TournamentTreeBracket extends preact.Component<{
if (node.team && !node.team1 && !node.team2) { if (node.team && !node.team1 && !node.team2) {
const rect = elem.append('svg:rect').classed('tournament-bracket-tree-draw', true) const rect = elem.append('svg:rect').classed('tournament-bracket-tree-draw', true)
.attr('rx', nodeSize.radius) .attr('rx', nodeSize.radius)
.attr('x', -nodeSize.realWidth / 2).attr('width', nodeSize.realWidth); .attr('x', -nodeSize.width / 2).attr('width', nodeSize.width)
rect.attr('y', -nodeSize.smallRealHeight / 2).attr('height', nodeSize.smallRealHeight); .attr('y', -nodeSize.height / 2).attr('height', nodeSize.height);
if (node.team === name) rect.attr('stroke-dasharray', '5,5').attr('stroke-width', 2); if (node.team === name) rect.attr('stroke-dasharray', '5,5').attr('stroke-width', 2);
elem.append('svg:text').classed('tournament-bracket-tree-node-team', true) elem.append('svg:text').classed('tournament-bracket-tree-node-team', true)
.attr('y', nodeSize.textOffset)
.classed('tournament-bracket-tree-node-team-draw', true) .classed('tournament-bracket-tree-node-team-draw', true)
.text(node.team || ''); .text(node.team || '');
} else { } else {
const rect1 = elem.append('svg:rect') const rect1 = elem.append('svg:rect')
.attr('rx', nodeSize.radius) .attr('rx', nodeSize.radius)
.attr('x', -nodeSize.realWidth / 2).attr('width', nodeSize.realWidth) .attr('x', -nodeSize.width / 2).attr('width', nodeSize.width)
.attr('y', -nodeSize.smallRealHeight).attr('height', nodeSize.smallRealHeight); .attr('y', -nodeSize.height).attr('height', nodeSize.height);
const rect2 = elem.append('svg:rect') const rect2 = elem.append('svg:rect')
.attr('rx', nodeSize.radius) .attr('rx', nodeSize.radius)
.attr('x', -nodeSize.realWidth / 2).attr('width', nodeSize.realWidth) .attr('x', -nodeSize.width / 2).attr('width', nodeSize.width)
.attr('y', 0).attr('height', nodeSize.smallRealHeight); .attr('y', 0).attr('height', nodeSize.height);
if (node.team1 === name) rect1.attr('stroke-dasharray', '5,5').attr('stroke-width', 2); if (node.team1 === name) rect1.attr('stroke-dasharray', '5,5').attr('stroke-width', 2);
if (node.team2 === name) rect2.attr('stroke-dasharray', '5,5').attr('stroke-width', 2); if (node.team2 === name) rect2.attr('stroke-dasharray', '5,5').attr('stroke-width', 2);
const row1 = elem.append('svg:text').attr('y', -nodeSize.realHeight / 4 + nodeSize.textOffset) const row1 = elem.append('svg:text').attr('y', -nodeSize.height / 2 + nodeSize.textOffset)
.classed('tournament-bracket-tree-node-row1', true); .classed('tournament-bracket-tree-node-row1', true);
const row2 = elem.append('svg:text').attr('y', nodeSize.realHeight / 4 + nodeSize.textOffset) const row2 = elem.append('svg:text').attr('y', nodeSize.height / 2 + nodeSize.textOffset)
.classed('tournament-bracket-tree-node-row2', true); .classed('tournament-bracket-tree-node-row2', true);
const team1 = row1.append('svg:tspan').classed('tournament-bracket-tree-team', true) const team1 = row1.append('svg:tspan').classed('tournament-bracket-tree-team', true)
@ -907,7 +921,7 @@ export class TournamentTreeBracket extends preact.Component<{
this.base!.appendChild(this.generateTreeBracket(this.props.data, this.props.abbreviated)); this.base!.appendChild(this.generateTreeBracket(this.props.data, this.props.abbreviated));
} }
override shouldComponentUpdate(props: { data: TournamentTreeBracketData }) { override shouldComponentUpdate(props: { data: TournamentTreeBracketData }) {
if (props.data === this.props.data && this.d3Loaded) return false; if (props.data === this.props.data && !this.d3Loader) return false;
this.base!.replaceChild(this.generateTreeBracket(props.data), this.base!.children[0]); this.base!.replaceChild(this.generateTreeBracket(props.data), this.base!.children[0]);
return false; return false;
} }
@ -923,7 +937,7 @@ export class TourPopOutPanel extends PSRoomPanel {
static readonly noURL = true; static readonly noURL = true;
override componentDidMount() { override componentDidMount() {
const tour = this.props.room.args?.tour as ChatTournament; const tour = this.props.room.args?.tour as ChatTournament;
if (tour) this.subscribeTo(tour, () => this.forceUpdate()); if (tour) this.subscribeTo(tour);
} }
override render() { override render() {
const room = this.props.room; const room = this.props.room;

View File

@ -8,7 +8,7 @@
import preact from "../js/lib/preact"; import preact from "../js/lib/preact";
import type { PSSubscription } from "./client-core"; import type { PSSubscription } from "./client-core";
import { PS, PSRoom, type RoomOptions, type RoomID, type Team } from "./client-main"; import { PS, PSRoom, type RoomOptions, type RoomID, type Team } from "./client-main";
import { PSMain, PSPanelWrapper, PSRoomPanel } from "./panels"; import { PSView, PSPanelWrapper, PSRoomPanel } from "./panels";
import { TeamForm } from "./panel-mainmenu"; import { TeamForm } from "./panel-mainmenu";
import { BattleLog } from "./battle-log"; import { BattleLog } from "./battle-log";
import type { Battle } from "./battle"; import type { Battle } from "./battle";
@ -113,7 +113,8 @@ export class ChatRoom extends PSRoom {
this.title = `Console`; this.title = `Console`;
} else if (this.id.startsWith('dm-')) { } else if (this.id.startsWith('dm-')) {
const id = this.id.slice(3); const id = this.id.slice(3);
if (!name || toID(name) !== id) name = this.pmTarget || id; if (toID(name) !== id) name = null;
name ||= this.pmTarget || id;
if (/[A-Za-z0-9]/.test(name.charAt(0))) name = ` ${name}`; if (/[A-Za-z0-9]/.test(name.charAt(0))) name = ` ${name}`;
const nameWithGroup = name; const nameWithGroup = name;
name = name.slice(1); name = name.slice(1);
@ -194,7 +195,7 @@ export class ChatRoom extends PSRoom {
if (!formatTargeting || if (!formatTargeting ||
formats[formatId] || formats[formatId] ||
gens[formatId.slice(0, 4)] || gens[formatId.slice(0, 4)] ||
(gens['gen6'] && formatId.substr(0, 3) !== 'gen')) { (gens['gen6'] && !formatId.startsWith('gen'))) {
buffer += '<tr>'; buffer += '<tr>';
} else { } else {
buffer += '<tr class="hidden">'; buffer += '<tr class="hidden">';
@ -704,10 +705,10 @@ export class ChatTextEntry extends preact.Component<{
onInput={this.update} onInput={this.update}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
style={{ resize: 'none', width: '100%', height: '16px', padding: '2px 3px 1px 3px' }} style={{ resize: 'none', width: '100%', height: '16px', padding: '2px 3px 1px 3px' }}
placeholder={PS.focusPreview(this.props.room)} placeholder={PSView.focusPreview(this.props.room)}
/> : <ChatTextBox /> : <ChatTextBox
disabled={!this.props.room.connected || !canTalk} disabled={!this.props.room.connected || !canTalk}
placeholder={PS.focusPreview(this.props.room)} placeholder={PSView.focusPreview(this.props.room)}
/>} />}
</form> </form>
{!canTalk && <button data-href="login" class="button autofocus"> {!canTalk && <button data-href="login" class="button autofocus">
@ -725,14 +726,14 @@ class ChatTextBox extends preact.Component<{ placeholder: string, disabled?: boo
return false; return false;
} }
handleFocus = () => { handleFocus = () => {
PSMain.setTextboxFocused(true); PSView.setTextboxFocused(true);
}; };
handleBlur = () => { handleBlur = () => {
PSMain.setTextboxFocused(false); PSView.setTextboxFocused(false);
}; };
override render() { override render() {
return <pre return <pre
class={`textbox textbox-empty ${this.props.disabled ? ' disabled' : ''}`} placeholder={this.props.placeholder} class={`textbox textbox-empty ${this.props.disabled ? ' disabled' : ' autofocus'}`} placeholder={this.props.placeholder}
onFocus={this.handleFocus} onBlur={this.handleBlur} onFocus={this.handleFocus} onBlur={this.handleBlur}
>{'\n'}</pre>; >{'\n'}</pre>;
} }
@ -745,10 +746,10 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
static readonly location = 'right'; static readonly location = 'right';
static readonly icon = <i class="fa fa-comment-o"></i>; static readonly icon = <i class="fa fa-comment-o"></i>;
override componentDidMount(): void { override componentDidMount(): void {
super.componentDidMount();
this.subscribeTo(PS.user, () => { this.subscribeTo(PS.user, () => {
this.props.room.updateTarget(); this.props.room.updateTarget();
}); });
super.componentDidMount();
} }
send = (text: string) => { send = (text: string) => {
this.props.room.send(text); this.props.room.send(text);
@ -818,7 +819,7 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
return <PSPanelWrapper room={room} focusClick> return <PSPanelWrapper room={room} focusClick>
<ChatLog class="chat-log" room={this.props.room} left={tinyLayout ? 0 : 146} top={room.tour?.info.isActive ? 30 : 0}> <ChatLog class="chat-log" room={this.props.room} left={tinyLayout ? 0 : 146} top={room.tour?.info.isActive ? 30 : 0}>
{challengeTo || challengeFrom && [challengeTo, challengeFrom]} {challengeTo}{challengeFrom}
</ChatLog> </ChatLog>
{room.tour && <TournamentBox tour={room.tour} left={tinyLayout ? 0 : 146} />} {room.tour && <TournamentBox tour={room.tour} left={tinyLayout ? 0 : 146} />}
<ChatTextEntry <ChatTextEntry

View File

@ -229,9 +229,7 @@ class LadderListPanel extends PSRoomPanel {
static readonly title = 'Ladder'; static readonly title = 'Ladder';
override componentDidMount() { override componentDidMount() {
this.subscribeTo(PS.teams, () => { this.subscribeTo(PS.teams);
this.forceUpdate();
});
} }
renderList() { renderList() {
if (!window.BattleFormats) { if (!window.BattleFormats) {

View File

@ -129,22 +129,19 @@ class UserPanel extends PSRoomPanel<UserRoom> {
buttonbar.push(isSelf ? ( buttonbar.push(isSelf ? (
<p class="buttonbar"> <p class="buttonbar">
<button class="button" disabled>Challenge</button> {} <button class="button" disabled>Challenge</button> {}
<button class="button" data-href="/dm-">Chat Self</button> <button class="button" data-href="dm-">Chat Self</button>
</p> </p>
) : !PS.user.named ? ( ) : !PS.user.named ? (
<p class="buttonbar"> <p class="buttonbar">
<button class="button" disabled>Challenge</button> {} <button class="button" disabled>Challenge</button> {}
<button class="button" disabled>Chat</button> <button class="button" disabled>Chat</button> {}
<button class="button" disabled>{'\u2026'}</button>
</p> </p>
) : ( ) : (
<p class="buttonbar"> <p class="buttonbar">
<button class="button" data-href={`/challenge-${user.userid}`}>Challenge</button> {} <button class="button" data-href={`challenge-${user.userid}`}>Challenge</button> {}
<button class="button" data-href={`/dm-${user.userid}`}>Chat</button> {} <button class="button" data-href={`dm-${user.userid}`}>Chat</button> {}
<button <button class="button" data-href={`useroptions-${user.userid}-${room.parentRoomid || ''}`}>{'\u2026'}</button>
class="button"
data-href="/useroptions"
value={`${room.userid as string},${room.parentRoomid as string}`}
>{'\u2026'}</button>
</p> </p>
)); ));
if (isSelf) { if (isSelf) {
@ -158,13 +155,13 @@ class UserPanel extends PSRoomPanel<UserRoom> {
} }
} }
const avatar = user.avatar !== '[loading]' ? Dex.resolveAvatar(`${user.avatar || 'unknown'}`) : null;
return [<div class="userdetails"> return [<div class="userdetails">
{user.avatar !== '[loading]' && {avatar && (room.isSelf ? (
<img <img src={avatar} class="trainersprite yours" data-href="avatars" />
{...(room.isSelf ? { 'data-href': 'avatars' } : {})} ) : (
class={'trainersprite' + (room.isSelf ? ' yours' : '')} <img src={avatar} class="trainersprite" />
src={Dex.resolveAvatar(`${user.avatar || 'unknown'}`)} ))}
/>}
<strong><a <strong><a
href={`//${Config.routes.users}/${user.userid}`} target="_blank" href={`//${Config.routes.users}/${user.userid}`} target="_blank"
style={{ color: away ? '#888888' : BattleLog.usernameColor(user.userid) }} style={{ color: away ? '#888888' : BattleLog.usernameColor(user.userid) }}
@ -201,7 +198,7 @@ class UserPanel extends PSRoomPanel<UserRoom> {
return <PSPanelWrapper room={room}><div class="pad"> return <PSPanelWrapper room={room}><div class="pad">
{showLookup && <form onSubmit={this.lookup} style={{ minWidth: '278px' }}> {showLookup && <form onSubmit={this.lookup} style={{ minWidth: '278px' }}>
<label class="label"> <label class="label">
Username: Username: {}
<input type="search" name="username" class="textbox autofocus" onInput={this.maybeReset} onChange={this.maybeReset} /> <input type="search" name="username" class="textbox autofocus" onInput={this.maybeReset} onChange={this.maybeReset} />
</label> </label>
{!room.userid && <p class="buttonbar"> {!room.userid && <p class="buttonbar">
@ -218,7 +215,7 @@ class UserPanel extends PSRoomPanel<UserRoom> {
class UserOptionsPanel extends PSRoomPanel { class UserOptionsPanel extends PSRoomPanel {
static readonly id = 'useroptions'; static readonly id = 'useroptions';
static readonly routes = ['useroptions']; static readonly routes = ['useroptions-*'];
static readonly location = 'popup'; static readonly location = 'popup';
static readonly noURL = true; static readonly noURL = true;
declare state: { declare state: {
@ -228,6 +225,12 @@ class UserOptionsPanel extends PSRoomPanel {
requestSent?: boolean, requestSent?: boolean,
data?: Record<string, string>, data?: Record<string, string>,
}; };
getTargets() {
const [, targetUser, targetRoomid] = this.props.room.id.split('-');
let targetRoom = (PS.rooms[targetRoomid] || null) as ChatRoom | null;
if (targetRoom?.type !== 'chat') targetRoom = null;
return { targetUser: targetUser as ID, targetRoomid: targetRoomid as RoomID, targetRoom };
}
handleMute = (ev: Event) => { handleMute = (ev: Event) => {
this.setState({ showMuteInput: true, showBanInput: false }); this.setState({ showMuteInput: true, showBanInput: false });
@ -247,52 +250,42 @@ class UserOptionsPanel extends PSRoomPanel {
}; };
handleConfirm = (ev: Event) => { handleConfirm = (ev: Event) => {
let data = this.state.data; const data = this.state.data;
if (!data) return; if (!data) return;
let roomid = toRoomid(data.room); const { targetUser, targetRoom } = this.getTargets();
let room = PS.rooms[roomid];
let cmd = ''; let cmd = '';
if (data.action === "Mute") { if (data.action === "Mute") {
cmd += data.duration === "1 hour" ? "/hourmute " : "/mute "; cmd += data.duration === "1 hour" ? "/hourmute " : "/mute ";
cmd += `${data.targetUser} ${data.reason ? ',' + data.reason : ''}`; cmd += `${targetUser} ${data.reason ? ',' + data.reason : ''}`;
} else { } else {
cmd += data.duration === "1 week" ? "/weekban " : "/ban "; cmd += data.duration === "1 week" ? "/weekban " : "/ban ";
cmd += `${data.targetUser} ${data.reason ? ',' + data.reason : ''}`; cmd += `${targetUser} ${data.reason ? ',' + data.reason : ''}`;
} }
room?.send(cmd); targetRoom?.send(cmd);
this.close(); this.close();
}; };
handleAddFriend = (ev: Event) => { handleAddFriend = (ev: Event) => {
let args = (this.props.room?.parentElem as HTMLInputElement).value.split(","); const { targetUser, targetRoom } = this.getTargets();
let [targetUser, roomid] = args; targetRoom?.send(`/friend add ${targetUser}`);
PS.rooms[roomid]?.send(`/friend add ${targetUser}`);
this.setState({ requestSent: true }); this.setState({ requestSent: true });
ev.preventDefault(); ev.preventDefault();
ev.stopImmediatePropagation(); ev.stopImmediatePropagation();
}; };
handleIgnore = () => { handleIgnore = () => {
let args = (this.props.room?.parentElem as HTMLInputElement).value.split(","); const { targetUser, targetRoom } = this.getTargets();
let [targetUser, roomid] = args; targetRoom?.send(`/ignore ${targetUser}`);
let room = PS.rooms[roomid];
room?.send(`/ignore ${targetUser}`);
this.close(); this.close();
}; };
muteUser = (ev: Event) => { muteUser = (ev: Event) => {
this.setState({ showMuteInput: false }); this.setState({ showMuteInput: false });
let hrMute = (ev.currentTarget as HTMLButtonElement).value === "1hr"; const hrMute = (ev.currentTarget as HTMLButtonElement).value === "1hr";
let args = (this.props.room?.parentElem as HTMLInputElement).value.split(","); const reason = this.base?.querySelector<HTMLInputElement>("input[name=mutereason]")?.value;
let [targetUser, roomid] = args; const data = {
let room = PS.rooms[roomid];
if (room?.type !== "chat") return; // should never happen
let reason = this.base?.querySelector<HTMLInputElement>("input[name=mutereason]")?.value;
let data = {
action: 'Mute', action: 'Mute',
targetUser,
room: room?.title,
reason, reason,
duration: hrMute ? "1 hour" : "7 minutes", duration: hrMute ? "1 hour" : "7 minutes",
}; };
@ -303,16 +296,10 @@ class UserOptionsPanel extends PSRoomPanel {
banUser = (ev: Event) => { banUser = (ev: Event) => {
this.setState({ showBanInput: false }); this.setState({ showBanInput: false });
let weekBan = (ev.currentTarget as HTMLButtonElement).value === "1wk"; const weekBan = (ev.currentTarget as HTMLButtonElement).value === "1wk";
let args = (this.props.room?.parentElem as HTMLInputElement).value.split(","); const reason = this.base?.querySelector<HTMLInputElement>("input[name=banreason]")?.value;
let [targetUser, roomid] = args; const data = {
let room = PS.rooms[roomid];
if (room?.type !== "chat") return; // should never happen
let reason = this.base?.querySelector<HTMLInputElement>("input[name=banreason]")?.value;
let data = {
action: 'Ban', action: 'Ban',
targetUser,
room: room?.title,
reason, reason,
duration: weekBan ? "1 week" : "2 days", duration: weekBan ? "1 week" : "2 days",
}; };
@ -321,20 +308,16 @@ class UserOptionsPanel extends PSRoomPanel {
ev.stopImmediatePropagation(); ev.stopImmediatePropagation();
}; };
update = () => {
this.forceUpdate();
};
override render() { override render() {
const room = this.props.room; const room = this.props.room;
const parentRoom = PS.rooms[this.props.room.parentRoomid! || ''] as ChatRoom;
let canMute = false; let canMute = false;
let canBan = false; let canBan = false;
if (parentRoom?.type === "chat") { const { targetUser, targetRoom } = this.getTargets();
let banPerms = ["@", "#", "~"]; if (targetRoom) {
let mutePerms = ["%", ...banPerms]; const banPerms = ["@", "#", "~"];
canMute = mutePerms.includes(parentRoom.users[PS.user.userid].charAt(0)); const mutePerms = ["%", ...banPerms];
canBan = banPerms.includes(parentRoom.users[PS.user.userid].charAt(0)); canMute = mutePerms.includes(targetRoom.users[PS.user.userid]?.charAt(0));
canBan = banPerms.includes(targetRoom.users[PS.user.userid]?.charAt(0));
} }
return <PSPanelWrapper room={room} width={280}><div class="pad"> return <PSPanelWrapper room={room} width={280}><div class="pad">
@ -344,16 +327,15 @@ class UserOptionsPanel extends PSRoomPanel {
</button> </button>
</p> </p>
<p> <p>
<button <button data-href={`view-help-request-report-user-${targetUser}`} class="button">
class="button"
data-href={`view-help-request-report-user-${(room.parentElem as HTMLInputElement).value.split(",")[0]}`}
>
Report Report
</button> </button>
</p> </p>
<p> <p>
{this.state.requestSent ? ( {this.state.requestSent ? (
<button class="button disabled"> Sent request </button> <button class="button disabled">
Sent request
</button>
) : ( ) : (
<button onClick={this.handleAddFriend} class="button"> <button onClick={this.handleAddFriend} class="button">
Add friend Add friend
@ -378,7 +360,8 @@ class UserOptionsPanel extends PSRoomPanel {
<p class="buttonbar"> <p class="buttonbar">
{canMute && !this.state.showBanInput && !this.state.showConfirm && (this.state.showMuteInput ? ( {canMute && !this.state.showBanInput && !this.state.showConfirm && (this.state.showMuteInput ? (
<div> <div>
<label class="inputlabel"> Reason: <label class="label">
Reason: {}
<input name="mutereason" class="textbox autofocus" placeholder="Mute reason (optional)" /> <input name="mutereason" class="textbox autofocus" placeholder="Mute reason (optional)" />
</label> {} <br /> </label> {} <br />
<button class="button" onClick={this.muteUser} value="7min">For 7 Mins</button> {} <button class="button" onClick={this.muteUser} value="7min">For 7 Mins</button> {}
@ -392,9 +375,10 @@ class UserOptionsPanel extends PSRoomPanel {
))} {} ))} {}
{canBan && !this.state.showMuteInput && !this.state.showConfirm && (this.state.showBanInput ? ( {canBan && !this.state.showMuteInput && !this.state.showConfirm && (this.state.showBanInput ? (
<div> <div>
<label class="inputlabel"> Reason: <label class="label">
Reason: {}
<input name="banreason" class="textbox autofocus" placeholder="Ban reason (optional)" /> <input name="banreason" class="textbox autofocus" placeholder="Ban reason (optional)" />
</label> <br /> </label><br />
<button class="button" onClick={this.banUser} value="2d">For 2 Days</button> {} <button class="button" onClick={this.banUser} value="2d">For 2 Days</button> {}
<button class="button" onClick={this.banUser} value="1wk">For 1 Week</button> {} <button class="button" onClick={this.banUser} value="1wk">For 1 Week</button> {}
<button class="button" onClick={this.handleCancel}>Cancel</button> <button class="button" onClick={this.handleCancel}>Cancel</button>
@ -483,6 +467,10 @@ class OptionsPanel extends PSRoomPanel {
static readonly location = 'popup'; static readonly location = 'popup';
declare state: { showStatusInput?: boolean, showStatusUpdated?: boolean }; declare state: { showStatusInput?: boolean, showStatusUpdated?: boolean };
override componentDidMount() {
super.componentDidMount();
this.subscribeTo(PS.user);
}
setTheme = (e: Event) => { setTheme = (e: Event) => {
const theme = (e.currentTarget as HTMLSelectElement).value as 'light' | 'dark' | 'system'; const theme = (e.currentTarget as HTMLSelectElement).value as 'light' | 'dark' | 'system';
PS.prefs.set('theme', theme); PS.prefs.set('theme', theme);
@ -493,6 +481,7 @@ class OptionsPanel extends PSRoomPanel {
switch (layout) { switch (layout) {
case '': case '':
PS.prefs.set('onepanel', null); PS.prefs.set('onepanel', null);
PS.rightPanel ||= PS.rooms['rooms'] || null;
break; break;
case 'onepanel': case 'onepanel':
PS.prefs.set('onepanel', true); PS.prefs.set('onepanel', true);
@ -580,7 +569,7 @@ class OptionsPanel extends PSRoomPanel {
</p> </p>
)} )}
{PS.user.named && (PS.user.registered ? {PS.user.named && (PS.user.registered?.userid === PS.user.userid ?
<button className="button" data-href="changepassword">Password...</button> : <button className="button" data-href="changepassword">Password...</button> :
<button className="button" data-href="register">Register</button>)} <button className="button" data-href="register">Register</button>)}
@ -767,7 +756,8 @@ class LoginPanel extends PSRoomPanel {
</p>} </p>}
{loginState?.needsPassword && <p> {loginState?.needsPassword && <p>
<i class="fa fa-level-up fa-rotate-90"></i> <strong>if you registered this name:</strong> <i class="fa fa-level-up fa-rotate-90"></i> <strong>if you registered this name:</strong>
<label class="label">Password: {} <label class="label">
Password: {}
<input <input
class="textbox" type={this.state.passwordShown ? 'text' : 'password'} name="password" class="textbox" type={this.state.passwordShown ? 'text' : 'password'} name="password"
autocomplete="current-password" style="width:173px" autocomplete="current-password" style="width:173px"
@ -960,7 +950,9 @@ class ReplacePlayerPanel extends PSRoomPanel {
<input name="newplayer" class="textbox autofocus" /> <input name="newplayer" class="textbox autofocus" />
</p> </p>
<p> <p>
<button type="submit" class="button"><strong>Replace</strong></button> {} <button type="submit" class="button">
<strong>Replace</strong>
</button> {}
<button type="button" data-cmd="/close" class="button"> <button type="button" data-cmd="/close" class="button">
Cancel Cancel
</button> </button>
@ -1017,58 +1009,36 @@ class ChangePasswordPanel extends PSRoomPanel {
</p> } </p> }
<p>Change your password:</p> <p>Change your password:</p>
<p> <p>
<label class="label">Username: <label class="label">
<strong><input Username: {}
type="text" <input name="username" value={PS.user.name} readOnly={true} autocomplete="username" class="textbox disabled" />
name="username" </label>
value={PS.user.name}
style="
color: inherit;
background: transparent;
border: 0;
font: inherit;
font-size: inherit;
display: block;
"
readOnly={true}
autocomplete="username"
/></strong></label>
</p> </p>
<p> <p>
<label class="label">Old password: <label class="label">
<input Old password: {}
class="textbox autofocus" <input name="oldpassword" type="password" autocomplete="current-password" class="textbox autofocus" />
type="password" </label>
name="oldpassword"
autocomplete="current-password"
/></label>
</p> </p>
<p> <p>
<label class="label">New password: <label class="label">
<input New password: {}
class="textbox" <input name="password" type="password" autocomplete="new-password" class="textbox" />
type="password" </label>
name="password"
autocomplete="new-password"
/></label>
</p> </p>
<p> <p>
<label class="label">New password (confirm): <label class="label">
<input New password (confirm): {}
class="textbox" <input name="cpassword" type="password" autocomplete="new-password" class="textbox" />
type="password" </label>
name="cpassword"
autocomplete="new-password"
/></label>
</p> </p>
<p class="buttonbar"> <p class="buttonbar">
<button type="submit" class="button"> <button type="submit" class="button">
<strong>Change password</strong> <strong>Change password</strong>
</button> </button> {}
<button type="button" data-cmd="/close" class="button">Cancel</button> <button type="button" data-cmd="/close" class="button">Cancel</button>
</p> </p>
</form> </form>
</div> </div>
</PSPanelWrapper>; </PSPanelWrapper>;
} }
@ -1083,10 +1053,6 @@ class RegisterPanel extends PSRoomPanel {
declare state: { errorMsg: string }; declare state: { errorMsg: string };
update = () => {
this.forceUpdate();
};
handleRegisterUser = (ev: Event) => { handleRegisterUser = (ev: Event) => {
ev.preventDefault(); ev.preventDefault();
let captcha = this.base?.querySelector<HTMLInputElement>('input[name=captcha]')?.value; let captcha = this.base?.querySelector<HTMLInputElement>('input[name=captcha]')?.value;
@ -1130,55 +1096,37 @@ class RegisterPanel extends PSRoomPanel {
</p> } </p> }
<p>Register your account:</p> <p>Register your account:</p>
<p> <p>
<label class="label">Username: <label class="label">
<strong><input Username: {}
type="text" <input name="name" value={PS.user.name} readOnly={true} autocomplete="username" class="textbox disabled" />
name="name" </label>
value={PS.user.name}
style="
color: inherit;
background: transparent;
border: 0;
font: inherit;
font-size: inherit;
display: block;
"
readOnly={true}
autocomplete="username"
/></strong></label>
</p> </p>
<p> <p>
<label class="label">Password: <label class="label">
<input Password: {}
class="textbox autofocus" <input name="password" type="password" autocomplete="new-password" class="textbox autofocus" />
type="password" </label>
name="password"
autocomplete="new-password"
/></label>
</p> </p>
<p> <p>
<label class="label">Password (confirm): <label class="label">
<input Password (confirm): {}
class="textbox" <input name="cpassword" type="password" autocomplete="new-password" class="textbox" />
type="password" </label>
name="cpassword"
autocomplete="new-password"
/></label>
</p> </p>
<p> <p>
<label class="label"> <img <label class="label"><img
src="https://play.pokemonshowdown.com/sprites/gen5ani/pikachu.gif" src="https://play.pokemonshowdown.com/sprites/gen5ani/pikachu.gif"
alt="An Electric-type mouse that is the mascot of the Pokémon franchise." alt="An Electric-type mouse that is the mascot of the Pokémon franchise."
/></label> /></label>
</p> </p>
<p> <p>
<label class="label">What is this pokemon? <label class="label">
<input What is this pokemon?{}
class="textbox" type="text" name="captcha" value="" <input name="captcha" class="textbox" />
/></label> </label>
</p> </p>
<p class="buttonbar"> <p class="buttonbar">
<button type="submit" class="button"><strong>Register</strong></button> <button type="submit" class="button"><strong>Register</strong></button> {}
<button type="button" data-cmd="/close" class="button">Cancel</button> <button type="button" data-cmd="/close" class="button">Cancel</button>
</p> </p>
</form> </form>

View File

@ -233,7 +233,7 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
<i class="fa fa-chevron-left"></i> List <i class="fa fa-chevron-left"></i> List
</button> </button>
<label class="label teamname"> <label class="label teamname">
Team name: Team name:{}
<input <input
class="textbox" type="text" value={team.name} onInput={this.rename} onChange={this.rename} onKeyUp={this.rename} class="textbox" type="text" value={team.name} onInput={this.rename} onChange={this.rename} onKeyUp={this.rename}
/> />

View File

@ -11,7 +11,7 @@
import preact from "../js/lib/preact"; import preact from "../js/lib/preact";
import { PS, type PSRoom, type RoomID } from "./client-main"; import { PS, type PSRoom, type RoomID } from "./client-main";
import { PSMain } from "./panels"; import { PSView } from "./panels";
import type { Battle } from "./battle"; import type { Battle } from "./battle";
import { BattleLog } from "./battle-log"; import { BattleLog } from "./battle-log";
@ -194,7 +194,7 @@ export class PSHeader extends preact.Component<{ style: object }> {
</span>; </span>;
} }
renderVertical() { renderVertical() {
return <div id="header" class="header-vertical" style={this.props.style} onClick={PSMain.scrollToHeader}> return <div id="header" class="header-vertical" style={this.props.style} onClick={PSView.scrollToHeader}>
<div class="maintabbarbottom"></div> <div class="maintabbarbottom"></div>
<div class="scrollable-part"> <div class="scrollable-part">
<img <img
@ -233,7 +233,7 @@ export class PSHeader extends preact.Component<{ style: object }> {
} }
override render() { override render() {
if (PS.leftPanelWidth === null) { if (PS.leftPanelWidth === null) {
if (!PSMain.textboxFocused) { if (!PSView.textboxFocused) {
document.documentElement.classList?.add('scroll-snap-enabled'); document.documentElement.classList?.add('scroll-snap-enabled');
} }
return this.renderVertical(); return this.renderVertical();
@ -301,11 +301,11 @@ export class PSMiniHeader extends preact.Component {
const menuButton = showMenuButton ? ( const menuButton = showMenuButton ? (
null null
) : window.scrollX ? ( ) : window.scrollX ? (
<button onClick={PSMain.scrollToHeader} class={`mini-header-left ${notifying}`} aria-label="Menu"> <button onClick={PSView.scrollToHeader} class={`mini-header-left ${notifying}`} aria-label="Menu">
<i class="fa fa-arrow-left"></i> <i class="fa fa-arrow-left"></i>
</button> </button>
) : ( ) : (
<button onClick={PSMain.scrollToRoom} class="mini-header-left" aria-label="Menu"> <button onClick={PSView.scrollToRoom} class="mini-header-left" aria-label="Menu">
<i class="fa fa-arrow-right"></i> <i class="fa fa-arrow-right"></i>
</button> </button>
); );
@ -319,4 +319,4 @@ export class PSMiniHeader extends preact.Component {
} }
} }
preact.render(<PSMain />, document.body, document.getElementById('ps-frame')!); preact.render(<PSView />, document.body, document.getElementById('ps-frame')!);

View File

@ -24,7 +24,7 @@ export class PSRouter {
panelState = ''; panelState = '';
constructor() { constructor() {
const currentRoomid = location.pathname.slice(1); const currentRoomid = location.pathname.slice(1);
if (/^[a-z0-9-]+$/.test(currentRoomid)) { if (/^[a-z0-9-]*$/.test(currentRoomid)) {
this.subscribeHistory(); this.subscribeHistory();
} else if (location.pathname.endsWith('.html')) { } else if (location.pathname.endsWith('.html')) {
this.subscribeHash(); this.subscribeHash();
@ -96,8 +96,6 @@ export class PSRouter {
const currentRoomid = location.hash.slice(1); const currentRoomid = location.hash.slice(1);
if (/^[a-z0-9-]+$/.test(currentRoomid)) { if (/^[a-z0-9-]+$/.test(currentRoomid)) {
PS.join(currentRoomid as RoomID); PS.join(currentRoomid as RoomID);
} else {
return;
} }
} }
PS.subscribeAndRun(() => { PS.subscribeAndRun(() => {
@ -120,8 +118,6 @@ export class PSRouter {
const currentRoomid = location.pathname.slice(1); const currentRoomid = location.pathname.slice(1);
if (/^[a-z0-9-]+$/.test(currentRoomid)) { if (/^[a-z0-9-]+$/.test(currentRoomid)) {
PS.join(currentRoomid as RoomID); PS.join(currentRoomid as RoomID);
} else {
return;
} }
if (!window.history) return; if (!window.history) return;
PS.subscribeAndRun(() => { PS.subscribeAndRun(() => {
@ -157,13 +153,14 @@ PS.router = new PSRouter();
export class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{ room: T }> { export class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{ room: T }> {
subscriptions: PSSubscription[] = []; subscriptions: PSSubscription[] = [];
subscribeTo<M>(model: PSModel<M> | PSStreamModel<M>, callback: (value: M) => void): PSSubscription { subscribeTo<M>(
model: PSModel<M> | PSStreamModel<M>, callback: (value: M) => void = () => { this.forceUpdate(); }
): PSSubscription {
const subscription = model.subscribe(callback); const subscription = model.subscribe(callback);
this.subscriptions.push(subscription); this.subscriptions.push(subscription);
return subscription; return subscription;
} }
override componentDidMount() { override componentDidMount() {
if (PS.room === this.props.room) this.focus();
this.props.room.onParentEvent = (id: string, e?: Event) => { this.props.room.onParentEvent = (id: string, e?: Event) => {
if (id === 'focus') this.focus(); if (id === 'focus') this.focus();
}; };
@ -171,7 +168,7 @@ export class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{ r
if (!args) this.forceUpdate(); if (!args) this.forceUpdate();
else this.receiveLine(args); else this.receiveLine(args);
})); }));
this.updateDimensions(); this.componentDidUpdate();
} }
justUpdatedDimensions = false; justUpdatedDimensions = false;
updateDimensions() { updateDimensions() {
@ -194,14 +191,11 @@ export class PSRoomPanel<T extends PSRoom = PSRoom> extends preact.Component<{ r
} }
override componentDidUpdate() { override componentDidUpdate() {
const room = this.props.room; const room = this.props.room;
if (this.base && ['popup', 'semimodal-popup'].includes(room.location)) { const currentlyHidden = !room.width && room.parentElem && ['popup', 'semimodal-popup'].includes(room.location);
if (room.width && room.hiddenInit) { this.updateDimensions();
room.hiddenInit = false; if (currentlyHidden) return;
this.focus(); if (room.focusNextUpdate) {
} room.focusNextUpdate = false;
this.updateDimensions();
} else if (this.base && room.hiddenInit) {
room.hiddenInit = false;
this.focus(); this.focus();
} }
} }
@ -259,49 +253,86 @@ export function PSPanelWrapper(props: {
} }
return <div return <div
id={`room-${room.id}`} class={'mini-window-contents ps-room-light' + (props.scrollable === true ? ' scrollable' : '')} id={`room-${room.id}`} class={'mini-window-contents ps-room-light' + (props.scrollable === true ? ' scrollable' : '')}
onClick={props.focusClick ? PSMain.focusIfNoSelection : undefined} onClick={props.focusClick ? PSView.focusIfNoSelection : undefined}
> >
{props.children} {props.children}
</div>; </div>;
} }
if (PS.isPopup(room)) { if (PS.isPopup(room)) {
const style = PSMain.getPopupStyle(room, props.width, props.fullSize); const style = PSView.getPopupStyle(room, props.width, props.fullSize);
return <div class="ps-popup" id={`room-${room.id}`} style={style}> return <div class="ps-popup" id={`room-${room.id}`} style={style}>
{props.children} {props.children}
</div>; </div>;
} }
const style = PSMain.posStyle(room) as any; const style = PSView.posStyle(room) as any;
if (props.scrollable === 'hidden') style.overflow = 'hidden'; if (props.scrollable === 'hidden') style.overflow = 'hidden';
return <div return <div
class={'ps-room' + (room.id === '' ? '' : ' ps-room-light') + (props.scrollable === true ? ' scrollable' : '')} class={'ps-room' + (room.id === '' ? '' : ' ps-room-light') + (props.scrollable === true ? ' scrollable' : '')}
id={`room-${room.id}`} id={`room-${room.id}`}
style={style} onClick={props.focusClick ? PSMain.focusIfNoSelection : undefined} style={style} onClick={props.focusClick ? PSView.focusIfNoSelection : undefined}
> >
{room.caughtError ? <div class="broadcast broadcast-red"><pre>{room.caughtError}</pre></div> : props.children} {room.caughtError ? <div class="broadcast broadcast-red"><pre>{room.caughtError}</pre></div> : props.children}
</div>; </div>;
} }
export class PSMain extends preact.Component { export class PSView extends preact.Component {
static readonly isChrome = navigator.userAgent.includes(' Chrome/'); static readonly isChrome = navigator.userAgent.includes(' Chrome/');
static readonly isSafari = !this.isChrome && navigator.userAgent.includes(' Safari/'); static readonly isSafari = !this.isChrome && navigator.userAgent.includes(' Safari/');
static readonly isMac = navigator.platform?.startsWith('Mac');
static textboxFocused = false; static textboxFocused = false;
static setTextboxFocused(focused: boolean) { static setTextboxFocused(focused: boolean) {
if (!PSMain.isChrome || PS.leftPanelWidth !== null) return; if (!PSView.isChrome || PS.leftPanelWidth !== null) return;
// Chrome bug: on Android, it insistently scrolls everything leftmost when scroll snap is enabled // Chrome bug: on Android, it insistently scrolls everything leftmost when scroll snap is enabled
this.textboxFocused = focused; this.textboxFocused = focused;
if (focused) { if (focused) {
document.documentElement.classList.remove('scroll-snap-enabled'); document.documentElement.classList.remove('scroll-snap-enabled');
PSMain.scrollToRoom(); PSView.scrollToRoom();
} else { } else {
document.documentElement.classList.add('scroll-snap-enabled'); document.documentElement.classList.add('scroll-snap-enabled');
} }
} }
static focusPreview(room: PSRoom) {
if (room !== PS.room) return '';
const verticalBuf = this.verticalFocusPreview();
if (verticalBuf) return verticalBuf;
const isMiniRoom = PS.room.location === 'mini-window';
const { rooms, index } = PS.horizontalNav();
if (index === -1) return '';
let buf = ' ';
const leftRoom = PS.rooms[rooms[index - 1]];
if (leftRoom) buf += `\u2190 ${leftRoom.title}`;
buf += (PS.arrowKeysUsed || isMiniRoom ? " | " : " (use arrow keys) ");
const rightRoom = PS.rooms[rooms[index + 1]];
if (rightRoom) buf += `${rightRoom.title} \u2192`;
return buf;
}
static verticalFocusPreview() {
const { rooms, index } = PS.verticalNav();
if (index === -1) return '';
const upRoom = PS.rooms[rooms[index - 1]];
let downRoom = PS.rooms[rooms[index + 1]];
if (index === rooms.length - 2 && rooms[index + 1] === 'news') downRoom = undefined;
if (!upRoom && !downRoom) return '';
let buf = ' ';
// const altLabel = PSMain.isMac ? '⌥' : 'ᴀʟᴛ';
const altLabel = PSView.isMac ? 'ᴏᴘᴛ' : 'ᴀʟᴛ';
if (upRoom) buf += `${altLabel}\u2191 ${upRoom.title}`;
buf += " | ";
if (downRoom) buf += `${altLabel}\u2193 ${downRoom.title}`;
return buf;
}
constructor() { constructor() {
super(); super();
PS.subscribe(() => this.forceUpdate()); PS.subscribe(() => this.forceUpdate());
if (PSMain.isSafari) { if (PSView.isSafari) {
// I don't want to prevent users from being able to zoom, but iOS 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), // auto-zooms when focusing textboxes (unless the font size is 16px),
// and this apparently fixes it while still allowing zooming. // and this apparently fixes it while still allowing zooming.
@ -401,7 +432,7 @@ export class PSMain extends preact.Component {
PS.update(); PS.update();
} }
if (clickedRoom && !PS.isPopup(clickedRoom)) { if (clickedRoom && !PS.isPopup(clickedRoom)) {
PSMain.scrollToRoom(); PSView.scrollToRoom();
} }
}); });
@ -494,7 +525,7 @@ export class PSMain extends preact.Component {
} }
static scrollToRoom() { static scrollToRoom() {
if (document.documentElement.scrollWidth > document.documentElement.clientWidth && window.scrollX === 0) { if (document.documentElement.scrollWidth > document.documentElement.clientWidth && window.scrollX === 0) {
if (PSMain.isSafari && PS.leftPanelWidth === null) { if (PSView.isSafari && PS.leftPanelWidth === null) {
// Safari bug: `scrollBy` doesn't actually work when scroll snap is enabled // Safari bug: `scrollBy` doesn't actually work when scroll snap is enabled
// note: interferes with the `PSMain.textboxFocused` workaround for a Chrome bug // note: interferes with the `PSMain.textboxFocused` workaround for a Chrome bug
document.documentElement.classList.remove('scroll-snap-enabled'); document.documentElement.classList.remove('scroll-snap-enabled');
@ -600,7 +631,7 @@ export class PSMain extends preact.Component {
return { maxWidth: width || 480 }; return { maxWidth: width || 480 };
} }
if (!room.width || !room.height) { if (!room.width || !room.height) {
room.hiddenInit = true; room.focusNextUpdate = true;
return { return {
position: 'absolute', position: 'absolute',
visibility: 'hidden', visibility: 'hidden',

View File

@ -59,7 +59,7 @@
* Buttons * Buttons
*********************************************************/ *********************************************************/
button, summary { a, button, summary {
cursor: pointer; cursor: pointer;
touch-action: manipulation; touch-action: manipulation;
} }

View File

@ -1006,6 +1006,9 @@ p.or:after {
.roomlist { .roomlist {
max-width: 480px; max-width: 480px;
text-align: left; text-align: left;
list-style: none;
margin: 0;
padding: 0;
} }
.roomlist .subrooms { .roomlist .subrooms {
font-size: 8pt; font-size: 8pt;
@ -1344,7 +1347,6 @@ a.ilink.yours {
.tournament-bracket-tree-node text, .tournament-bracket-tree-node { .tournament-bracket-tree-node text, .tournament-bracket-tree-node {
font-size: 8.5pt; font-size: 8.5pt;
text-anchor: middle; text-anchor: middle;
dominant-baseline: central;
font-weight: bold; font-weight: bold;
} }
.tournament-bracket-tree-node a { .tournament-bracket-tree-node a {
@ -2131,6 +2133,7 @@ a.ilink.yours {
width: 16px; width: 16px;
color: #777777; color: #777777;
} }
.folderpane .text,
.folderpane h3 { .folderpane h3 {
margin: 0; margin: 0;
padding: 13px 0 0 0; padding: 13px 0 0 0;
@ -2140,6 +2143,10 @@ a.ilink.yours {
color: black; color: black;
border-right: 1px solid #888888; border-right: 1px solid #888888;
} }
.folderpane .text {
height: 23px;
padding: 7px 0 0 7px;
}
.folder .selectFolder { .folder .selectFolder {
display: block; display: block;
padding: 0 0 0 7px; padding: 0 0 0 7px;
@ -2185,6 +2192,7 @@ a.ilink.yours {
padding-left: 6px; padding-left: 6px;
border-top-left-radius: 3px; border-top-left-radius: 3px;
border-bottom-left-radius: 3px; border-bottom-left-radius: 3px;
text-decoration: none;
} }
/* believe me, there was no other way to do this */ /* believe me, there was no other way to do this */
@ -3215,6 +3223,7 @@ a.ilink.yours {
.dark .folderlistafter:before, .dark .folderlistafter:before,
.dark .folderlistbefore:before, .dark .folderlistbefore:before,
.dark .folderpane h3, .dark .folderpane h3,
.dark .folderpane .text,
.dark .folder .selectFolder, .dark .folder .selectFolder,
.dark .folderhack1, .dark .folderhack1,
.dark .folderhack2 { .dark .folderhack2 {

View File

@ -1228,6 +1228,7 @@ pre.textbox.textbox-empty[placeholder]:before {
width: 100%; width: 100%;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
white-space: nowrap;
} }
.tournament-title:hover { .tournament-title:hover {
background: rgba(242, 247, 250, 0.85); background: rgba(242, 247, 250, 0.85);
@ -1249,6 +1250,7 @@ pre.textbox.textbox-empty[placeholder]:before {
line-height: 30px; line-height: 30px;
font-weight: normal; font-weight: normal;
display: inline-block; display: inline-block;
font-size: 11px;
} }
.tournament-box { .tournament-box {
@ -1375,7 +1377,6 @@ pre.textbox.textbox-empty[placeholder]:before {
.tournament-bracket-tree-node text, .tournament-bracket-tree-node { .tournament-bracket-tree-node text, .tournament-bracket-tree-node {
font-size: 8.5pt; font-size: 8.5pt;
text-anchor: middle; text-anchor: middle;
dominant-baseline: central;
font-weight: bold; font-weight: bold;
} }
.tournament-bracket-tree-node a { .tournament-bracket-tree-node a {
@ -2331,104 +2332,3 @@ pre.textbox.textbox-empty[placeholder]:before {
.dark .chat.mine { .dark .chat.mine {
background: rgba(255,255,255,0.05); background: rgba(255,255,255,0.05);
} }
/* teambuilder */
.dark .folderpane {
border-left-color: #484848;
}
.dark .folderlist .foldersep:before,
.dark .folderlistafter:before,
.dark .folderlistbefore:before,
.dark .folderpane h3,
.dark .folder .selectFolder,
.dark .folderhack1,
.dark .folderhack2 {
background: #484848;
color: #ddd;
}
.dark .folder .selectFolder:hover {
background: #686868;
}
.dark .folderpane i {
color: #ddd;
}
.dark .folder .selectFolder.active,
.dark .folder .selectFolder:active {
background: #27333c;
color: white;
}
.dark .folder.cur .selectFolder {
background: transparent;
}
/* teambuilder set */
.dark .utilichart h3, .dark .dexentry h3, .dark .resultheader h3 {
background: #636363;
color: #F1F1F1;
border: 1px solid #A9A9A9;
text-shadow: 1px 1px 0 rgb(40, 43, 45);
box-shadow: inset 0px 1px 0 rgb(49, 49, 49);
border-right: none;
}
.dark .teambar button {
background: #5A6570;
color: #F1F1F1;
border: 1px solid #AAAAAA;
}
.dark .teambar button:hover {
background: #444C54;
color: #F1F1F1;
border: 1px solid #AAAAAA;
}
.dark .teambar button:disabled, .dark .teambar button:disabled:hover, .dark .teambar button:disabled:active {
color: #ffffff;
background: #2d343a;
border-color: #6bacc5;
opacity: 1;
}
.dark .teambuilder-results .result a.hover,
.dark .teambuilder-results .result a:hover,
.dark .setmenu button:hover,
.dark .teamlist button:hover {
border-color: #777777;
background: rgba(100, 100, 100, 0.5);
color: #FFFFFF;
}
.dark .teambuilder-results .result a.cur {
border-color: #BBBBBB;
background: rgba(100, 100, 100, 0.2);
}
.dark .teambuilder-results .result a.cur:hover {
border-color: #BBBBBB;
background: rgba(100, 100, 100, 0.4);
color: #FFFFFF;
}
.dark .setmenu button,
.dark .teamlist button,
.dark .folder.cur .selectFolder,
.dark .utilichart .namecol,
.dark .utilichart .pokemonnamecol,
.dark .utilichart .movenamecol {
color: #DDD;
}
.dark .utilichart .col {
color: #DDD;
}
.dark .utilichart .cur .col {
color: #FFF;
}
.dark .utilichart a:hover .col {
color: #FFF;
}

View File

@ -88,6 +88,7 @@
width: 16px; width: 16px;
color: #777777; color: #777777;
} }
.folderpane .text,
.folderpane h3 { .folderpane h3 {
margin: 0; margin: 0;
padding: 13px 0 0 0; padding: 13px 0 0 0;
@ -97,6 +98,10 @@
color: black; color: black;
border-right: 1px solid #888888; border-right: 1px solid #888888;
} }
.folderpane .text {
height: 23px;
padding: 7px 0 0 7px;
}
.folder .selectFolder { .folder .selectFolder {
display: block; display: block;
padding: 0 0 0 7px; padding: 0 0 0 7px;
@ -199,6 +204,41 @@
margin-right: 2px; margin-right: 2px;
} }
/* dark */
.dark .folderpane {
border-left-color: #484848;
}
.dark .folderlist .foldersep:before,
.dark .folderlistafter:before,
.dark .folderlistbefore:before,
.dark .folderpane h3,
.dark .folderpane .text,
.dark .folder .selectFolder,
.dark .folderhack1,
.dark .folderhack2 {
background: #484848;
color: #ddd;
}
.dark .folder .selectFolder:hover {
background: #686868;
}
.dark .folderpane i {
color: #ddd;
}
.dark .folder .selectFolder.active,
.dark .folder .selectFolder:active {
background: #27333c;
color: white;
}
.dark .folder.cur .selectFolder {
background: transparent;
}
/********************************************************* /*********************************************************
* Teambuilder editor * Teambuilder editor
@ -271,3 +311,69 @@
left: 10px; left: 10px;
right: 10px; right: 10px;
} }
/* teambuilder set */
.dark .utilichart h3, .dark .dexentry h3, .dark .resultheader h3 {
background: #636363;
color: #F1F1F1;
border: 1px solid #A9A9A9;
text-shadow: 1px 1px 0 rgb(40, 43, 45);
box-shadow: inset 0px 1px 0 rgb(49, 49, 49);
border-right: none;
}
.dark .teambar button {
background: #5A6570;
color: #F1F1F1;
border: 1px solid #AAAAAA;
}
.dark .teambar button:hover {
background: #444C54;
color: #F1F1F1;
border: 1px solid #AAAAAA;
}
.dark .teambar button:disabled, .dark .teambar button:disabled:hover, .dark .teambar button:disabled:active {
color: #ffffff;
background: #2d343a;
border-color: #6bacc5;
opacity: 1;
}
.dark .teambuilder-results .result a.hover,
.dark .teambuilder-results .result a:hover,
.dark .setmenu button:hover,
.dark .teamlist button:hover {
border-color: #777777;
background: rgba(100, 100, 100, 0.5);
color: #FFFFFF;
}
.dark .teambuilder-results .result a.cur {
border-color: #BBBBBB;
background: rgba(100, 100, 100, 0.2);
}
.dark .teambuilder-results .result a.cur:hover {
border-color: #BBBBBB;
background: rgba(100, 100, 100, 0.4);
color: #FFFFFF;
}
.dark .setmenu button,
.dark .teamlist button,
.dark .folder.cur .selectFolder,
.dark .utilichart .namecol,
.dark .utilichart .pokemonnamecol,
.dark .utilichart .movenamecol {
color: #DDD;
}
.dark .utilichart .col {
color: #DDD;
}
.dark .utilichart .cur .col {
color: #FFF;
}
.dark .utilichart a:hover .col {
color: #FFF;
}

View File

@ -121,10 +121,16 @@
<script src="js/panel-mainmenu.js"></script> <script src="js/panel-mainmenu.js"></script>
<script src="js/panel-rooms.js"></script> <script src="js/panel-rooms.js"></script>
<script src="js/panel-topbar.js"></script> <script src="js/panel-topbar.js"></script>
<script src="js/panel-popups.js"></script> <!-- at this point, the main view is loaded and usable -->
<script src="js/miniedit.js"></script> <script src="js/miniedit.js"></script>
<script src="js/panel-chat-tournament.js"></script> <script src="js/panel-chat-tournament.js"></script>
<script src="js/panel-chat.js"></script> <script src="js/panel-chat.js"></script>
<!-- at this point, chatrooms are usable -->
<script src="js/panel-popups.js"></script>
<script src="js/panel-page.js?"></script>
<script src="js/panel-ladder.js?"></script>
<script src="js/battle-sound.js"></script> <script src="js/battle-sound.js"></script>
<script src="js/lib/jquery-2.2.4.min.js"></script> <script src="js/lib/jquery-2.2.4.min.js"></script>
@ -146,11 +152,11 @@
<script src="js/battle-dex-search.js?"></script> <script src="js/battle-dex-search.js?"></script>
<script src="js/battle-searchresults.js?"></script> <script src="js/battle-searchresults.js?"></script>
<script src="js/panel-teambuilder-team.js?"></script> <script src="js/panel-teambuilder-team.js?"></script>
<script src="js/panel-ladder.js?"></script>
<script src="js/panel-page.js?"></script>
<script src="https://play.pokemonshowdown.com/data/pokedex-mini.js"></script> <script src="https://play.pokemonshowdown.com/data/pokedex-mini.js"></script>
<script src="https://play.pokemonshowdown.com/data/pokedex-mini-bw.js"></script> <script src="https://play.pokemonshowdown.com/data/pokedex-mini-bw.js"></script>
<script src="js/lib/d3.v3.min.js"></script> <script src="js/lib/d3.v3.min.js"></script>
<script src="js/client-endload.js?"></script>
</body></html> </body></html>

View File

@ -422,7 +422,7 @@ export class BattlePanel extends preact.Component<{ id: string }> {
</form> </form>
<p> <p>
<em>Pro tip:</em> You don't need to click "Skip to turn" if you have a keyboard, just start typing <em>Pro tip:</em> You don't need to click "Skip to turn" if you have a keyboard, just start typing
the turn number and press <kbd>Enter</kbd>. For more shortcuts, press <kbd>Shift</kbd>+<kbd>/</kbd> the turn number and press <kbd>Enter</kbd>. For more shortcuts, press <kbd>Shift</kbd>+<kbd>/</kbd> {}
when a text box isn't focused. when a text box isn't focused.
</p> </p>
</section></div>; </section></div>;